@@ -180,6 +180,31 @@ static int traceMissingCellAgainstPolygon(const GeoPolygon *polygon,
180180 return 0 ;
181181}
182182
183+ /* ------------------------------------------------------------------ */
184+ /* Helper: build the outer polygon for gridDisk(center, k). */
185+ /* Returns E_SUCCESS and populates *mpoly on success. */
186+ /* Caller must call destroyGeoMultiPolygon on E_SUCCESS. */
187+ /* ------------------------------------------------------------------ */
188+ static H3Error buildDiskPolygon (H3Index center , int k , GeoMultiPolygon * mpoly ) {
189+ int64_t diskMax = 0 ;
190+ H3Error err = H3_EXPORT (maxGridDiskSize )(k , & diskMax );
191+ if (err != E_SUCCESS ) return err ;
192+
193+ H3Index * disk = calloc ((size_t )diskMax , sizeof (H3Index ));
194+ if (!disk ) return E_MEMORY_ALLOC ;
195+
196+ err = H3_EXPORT (gridDisk )(center , k , disk );
197+ if (err != E_SUCCESS ) {
198+ free (disk );
199+ return err ;
200+ }
201+
202+ const int64_t diskCount = compactNonZeroCells (disk , diskMax );
203+ err = H3_EXPORT (cellsToMultiPolygon )(disk , diskCount , mpoly );
204+ free (disk );
205+ return err ;
206+ }
207+
183208/* ------------------------------------------------------------------ */
184209/* Helper: run the roundtrip check for one (cell, k) pair. */
185210/* Updates aggregate stats instead of asserting internally. */
@@ -340,4 +365,156 @@ SUITE(GeodesicGridDiskRoundtrip) {
340365 t_assert (total .missingCells == 0 ,
341366 "all exhaustive res/k configs should round-trip" );
342367 }
368+
369+ // Stage 1: for each res-0 and res-1 cell, build the k=1 disk polygon
370+ // (the union of the cell and its 6 neighbours) and run FULL geodesic
371+ // polyfill. The center cell has no edges on the outer polygon boundary, so
372+ // it must appear in the FULL result.
373+ TEST (ringPolygonFullContainment ) {
374+ const int resolutions [] = {0 , 1 };
375+ for (int ri = 0 ; ri < 2 ; ri ++ ) {
376+ int res = resolutions [ri ];
377+ for (IterCellsResolution it = iterInitRes (res ); it .h ;
378+ iterStepRes (& it )) {
379+ H3Index center = it .h ;
380+
381+ GeoMultiPolygon mpoly = {0 };
382+ if (buildDiskPolygon (center , 1 , & mpoly ) != E_SUCCESS ) continue ;
383+ if (mpoly .numPolygons != 1 || mpoly .polygons [0 ].numHoles != 0 ) {
384+ H3_EXPORT (destroyGeoMultiPolygon )(& mpoly );
385+ continue ;
386+ }
387+
388+ uint32_t flags = CONTAINMENT_FULL ;
389+ FLAG_SET_GEODESIC (flags );
390+
391+ int64_t outMax = 0 ;
392+ t_assertSuccess (H3_EXPORT (maxPolygonToCellsSizeExperimental )(
393+ & mpoly .polygons [0 ], res , flags , & outMax ));
394+
395+ H3Index * out =
396+ outMax > 0 ? calloc ((size_t )outMax , sizeof (H3Index )) : NULL ;
397+ t_assert (outMax == 0 || out != NULL , "alloc succeeded" );
398+ t_assertSuccess (H3_EXPORT (polygonToCellsExperimental )(
399+ & mpoly .polygons [0 ], res , flags , outMax , out ));
400+
401+ bool found = false;
402+ for (int64_t i = 0 ; i < outMax && !found ; i ++ ) {
403+ if (out [i ] == center ) found = true;
404+ }
405+ t_assert (found ,
406+ "center cell must be in FULL result of k=1 disk "
407+ "polygon" );
408+
409+ free (out );
410+ H3_EXPORT (destroyGeoMultiPolygon )(& mpoly );
411+ }
412+ }
413+ }
414+
415+ // Stage 2: same k=1 disk polygon but with a hole punched through the
416+ // center cell. The hole vertices are midpoints between each center-cell
417+ // boundary vertex and the cell center, so the hole lies entirely inside
418+ // the center cell.
419+ //
420+ // Expected behaviour:
421+ // FULL – center cell excluded (its center is inside the hole, so
422+ // point-in-polygon returns false and FULL skips it)
423+ // OVERLAPPING – center cell included (the hole's first vertex maps to
424+ // the center cell, detected by the hole-vertex fallback)
425+ // – all 6 neighbours included (their centers are inside the
426+ // filled region)
427+ TEST (ringPolygonHoleExcludes ) {
428+ for (IterCellsResolution it = iterInitRes (0 ); it .h ; iterStepRes (& it )) {
429+ H3Index center = it .h ;
430+ int res = H3_GET_RESOLUTION (center );
431+
432+ GeoMultiPolygon mpoly = {0 };
433+ if (buildDiskPolygon (center , 1 , & mpoly ) != E_SUCCESS ) continue ;
434+ if (mpoly .numPolygons != 1 || mpoly .polygons [0 ].numHoles != 0 ) {
435+ H3_EXPORT (destroyGeoMultiPolygon )(& mpoly );
436+ continue ;
437+ }
438+
439+ // Build the k=1 disk cells list to verify OVERLAPPING later.
440+ int64_t diskMax = 0 ;
441+ t_assertSuccess (H3_EXPORT (maxGridDiskSize )(1 , & diskMax ));
442+ H3Index * disk = calloc ((size_t )diskMax , sizeof (H3Index ));
443+ t_assert (disk != NULL , "alloc disk succeeded" );
444+ t_assertSuccess (H3_EXPORT (gridDisk )(center , 1 , disk ));
445+ const int64_t diskCount = compactNonZeroCells (disk , diskMax );
446+
447+ // Build hole: midpoints between boundary vertices and cell center,
448+ // with longitude wrapped to [-pi, pi] to handle transmeridian
449+ // cells.
450+ CellBoundary bnd = {0 };
451+ t_assertSuccess (H3_EXPORT (cellToBoundary )(center , & bnd ));
452+ LatLng cellCenter ;
453+ t_assertSuccess (H3_EXPORT (cellToLatLng )(center , & cellCenter ));
454+
455+ LatLng holeVerts [MAX_CELL_BNDRY_VERTS ];
456+ for (int i = 0 ; i < bnd .numVerts ; i ++ ) {
457+ double dlng = bnd .verts [i ].lng - cellCenter .lng ;
458+ if (dlng > M_PI ) dlng -= 2.0 * M_PI ;
459+ if (dlng < - M_PI ) dlng += 2.0 * M_PI ;
460+ holeVerts [i ].lat = (bnd .verts [i ].lat + cellCenter .lat ) / 2.0 ;
461+ holeVerts [i ].lng = cellCenter .lng + dlng / 2.0 ;
462+ }
463+ GeoLoop holeLoop = {.numVerts = bnd .numVerts , .verts = holeVerts };
464+
465+ // Attach the hole to the outer polygon.
466+ GeoPolygon polyWithHole = mpoly .polygons [0 ];
467+ polyWithHole .numHoles = 1 ;
468+ polyWithHole .holes = & holeLoop ;
469+
470+ uint32_t flagsFull = CONTAINMENT_FULL ;
471+ FLAG_SET_GEODESIC (flagsFull );
472+ uint32_t flagsOver = CONTAINMENT_OVERLAPPING ;
473+ FLAG_SET_GEODESIC (flagsOver );
474+
475+ // FULL: center cell must NOT be in result.
476+ int64_t fullMax = 0 ;
477+ t_assertSuccess (H3_EXPORT (maxPolygonToCellsSizeExperimental )(
478+ & polyWithHole , res , flagsFull , & fullMax ));
479+ H3Index * fullOut =
480+ fullMax > 0 ? calloc ((size_t )fullMax , sizeof (H3Index )) : NULL ;
481+ t_assert (fullMax == 0 || fullOut != NULL , "alloc full succeeded" );
482+ t_assertSuccess (H3_EXPORT (polygonToCellsExperimental )(
483+ & polyWithHole , res , flagsFull , fullMax , fullOut ));
484+
485+ bool inFull = false;
486+ for (int64_t i = 0 ; i < fullMax && !inFull ; i ++ ) {
487+ if (fullOut [i ] == center ) inFull = true;
488+ }
489+ t_assert (!inFull ,
490+ "center cell must NOT appear in FULL result when hole "
491+ "is inside it" );
492+ free (fullOut );
493+
494+ // OVERLAPPING: every k=1 disk cell (including center) must be in
495+ // result.
496+ int64_t overMax = 0 ;
497+ t_assertSuccess (H3_EXPORT (maxPolygonToCellsSizeExperimental )(
498+ & polyWithHole , res , flagsOver , & overMax ));
499+ H3Index * overOut =
500+ overMax > 0 ? calloc ((size_t )overMax , sizeof (H3Index )) : NULL ;
501+ t_assert (overMax == 0 || overOut != NULL , "alloc over succeeded" );
502+ t_assertSuccess (H3_EXPORT (polygonToCellsExperimental )(
503+ & polyWithHole , res , flagsOver , overMax , overOut ));
504+
505+ for (int64_t i = 0 ; i < diskCount ; i ++ ) {
506+ bool found = false;
507+ for (int64_t j = 0 ; j < overMax && !found ; j ++ ) {
508+ if (overOut [j ] == disk [i ]) found = true;
509+ }
510+ t_assert (found ,
511+ "every k=1 disk cell must be in OVERLAPPING result "
512+ "even with hole inside center cell" );
513+ }
514+ free (overOut );
515+
516+ free (disk );
517+ H3_EXPORT (destroyGeoMultiPolygon )(& mpoly );
518+ }
519+ }
343520}
0 commit comments