Fixing H3 Polygon Fill That Misses Cells or Returns Nothing
Problem statement
You fill a polygon with H3 cells and get less than you expected โ or nothing at all:
h3.geo_to_cells(vatican, 7) # []
h3.geo_to_cells(switzerland_lv95, 5) # [] no error
h3.geo_to_cells(antarctica, 5) # H3FailedError() with an empty message
h3.polygon_to_cells(switzerland, 5)
# ValueError: Unrecognized type: <class 'shapely.geometry.polygon.Polygon'>
The empty results are the dangerous ones, because nothing tells you they are wrong. Measured by filling all 258 Natural Earth countries with the default settings, 87 received no cells at resolution 3, 27 at resolution 5 and still 7 at resolution 7 โ among them the Vatican, the Coral Sea Islands and Scarborough Reef.
Partial results are worse still, because the count looks reasonable. At resolution 5, 136 of Indonesia's 264 polygon parts got no cell: 10,236 kmยฒ of islands missing from a fill that otherwise matched the country's area.
There are five distinct causes. Each has a symptom you can check for and a fix that was measured to work.
Quick answer
import h3
import shapely
gdf = gdf.to_crs(4326) # H3 fills need degrees
geom = shapely.make_valid(gdf.geometry.iloc[0])
cells = set()
for part in getattr(geom, "geoms", [geom]):
found = h3.geo_to_cells(part, res) # centre containment
if not found: # a part too small to hold a centre
found = h3.h3shape_to_cells_experimental(h3.geo_to_h3shape(part), res, contain="overlap")
cells.update(found)
For a polygon that surrounds a pole, cut it into longitude sectors before filling (Example 3). Measured on Antarctica's mainland at resolution 5, the whole polygon raised H3FailedError, while four 90ยฐ sectors returned 44,940 cells covering 12.06 million kmยฒ in 0.4 s.
Step-by-step solution
1. Know the default rule: a cell's centre must be inside
geo_to_cells and polygon_to_cells include a cell only when its centre point falls inside the polygon. That is a sensible default. In a set of adjacent polygons each cell lands in exactly one of them, and the total cell area closely matches the polygon's area:
polygon res centre overlap full
Greece 7 22,303 ( 99.9%) 24,793 (111.2%) 20,036 (89.7%)
Switzerland 7 8,317 ( 99.7%) 8,709 (104.4%) 7,927 (95.0%)
Malta 5 2 (177.9%) 7 (622.4%) 0 ( 0.0%)
The percentages are the cells' area as a share of the polygon's geodesic area. Centre containment is accurate for large shapes and erratic for small ones: Malta's 326 kmยฒ got two cells covering 580 kmยฒ, and full containment gave none.
2. Fix small polygons: raise the resolution or use overlap
A polygon smaller than a cell usually contains no centre. The Vatican, 0.012 kmยฒ, got no centre cell at resolution 7 or 8 and one at resolution 9.
Two fixes, with different meanings:
- A finer resolution keeps centre containment's accuracy. Choose one whose average cell area is well below the smallest polygon that must appear.
- Overlap containment includes every cell the polygon touches. The Vatican got one cell at resolution 7. It never drops a polygon, and in a coverage it assigns boundary cells to more than one neighbour.
The overlap mode is exposed as h3shape_to_cells_experimental(shape, res, contain="overlap") in h3 4.5. The other options are "center", "full" and "bbox_overlap". The _experimental suffix is part of the name, so pin the version if you depend on it.
3. Fix multi-part polygons that lose their small parts
A MultiPolygon is filled as a whole, so every part too small for a centre vanishes, however large the rest is:
country res parts parts with no cell area lost
Indonesia 5 264 136 10,236 kmยฒ
Philippines 5 97 51 3,826 kmยฒ
Greece 6 74 17 280 kmยฒ
Norway 6 120 8 141 kmยฒ
Switching the whole country to overlap containment fixes the islands and inflates the mainland: Indonesia went from 7,037 cells to 9,036, 129% of its area. Rescue only the empty parts instead, and keep centre containment everywhere else โ 319 extra cells for Indonesia, 53 for Greece (Example 2).
4. Fix projected coordinates: reproject to EPSG:4326
H3 reads coordinates as longitude and latitude in degrees. Switzerland in the Swiss LV95 CRS has coordinates around 2,485,468 by 1,075,552, and geo_to_cells returned 0 cells and no error. Always to_crs(4326) first, and check that the bounds are within ยฑ180 and ยฑ90 before filling.
5. Fix the input type: shapely goes to geo_to_cells
h3 4.x has two entry points. geo_to_cells accepts shapely geometries and GeoJSON-like mappings. polygon_to_cells accepts only H3's own LatLngPoly and LatLngMultiPoly shapes, with (lat, lng) pairs, and rejects anything else:
polygon_to_cells(shapely polygon) ValueError: Unrecognized type: <class 'shapely.geometry.polygon.Polygon'>
polygon_to_cells(GeoJSON dict) ValueError: Unrecognized type: <class 'dict'>
LatLngPoly with 2 points ValueError: Non-empty LatLngPoly loops need at least 3 points.
Building LatLngPoly by hand from a shapely ring is where coordinates get swapped, so prefer geo_to_cells whenever you start from shapely.
6. Fix polygons that surround a pole: fill in sectors
Antarctica's mainland polygon runs from โ180ยฐ to 180ยฐ and down to โ89.99999999999994ยฐ โ a ring around the South Pole. It filled at resolutions 0 to 4 (6,431 cells at resolution 4) and raised H3FailedError from resolution 5 upwards. The exception's message is an empty string.
Clipping the polar tip off does not reliably help, and one version is worse than failing:
clipped at โ89.99ยฐ 23,515 cells, no error about half of the 44,940 it should be
clipped at โ86ยฐ H3FailedError
four 90ยฐ sectors 44,940 cells, 0 failures, 12.06 M kmยฒ in 0.4 s
Cut the polygon into longitude sectors, fill each, and union the cells. Eight 45ยฐ sectors gave the identical 44,940 cells. The polygon's own geodesic area is 12.16 million kmยฒ; centre containment accounts for the small difference.
7. Rule out swapped coordinates
A fill that returns a plausible number of cells in the wrong place is a coordinate-order bug, not a fill bug. Switzerland's ring passed to LatLngPoly in shapely's (lng, lat) order filled 13,851 cells near the Horn of Africa instead of 8,317 in the Alps. Check where the cells are, not only how many.
8. Holes are handled for you
Holes are subtracted correctly. Italy's mainland polygon has two, San Marino and the Vatican. At resolution 9 the fill with holes had 548 fewer cells than the outer ring alone, and all 547 of San Marino's own cells were among them.
Code examples
Example 1 โ reject the inputs that fail silently
def check_fill_input(geom, crs=None):
"""Reject the inputs that make H3 fills silently empty or fail."""
if crs is not None and not getattr(crs, "is_geographic", False):
raise ValueError(f"geometry is in {crs}; reproject to EPSG:4326 first")
minx, miny, maxx, maxy = geom.bounds
if abs(miny) > 90 or abs(maxy) > 90 or abs(minx) > 180 or abs(maxx) > 180:
raise ValueError(f"bounds {geom.bounds} are not degrees - is this a projected CRS?")
if (miny < -89.9 or maxy > 89.9) and maxx - minx > 359:
raise ValueError("the polygon wraps a pole; fill it in longitude sectors (see fill_in_sectors)")
if geom.is_empty or not geom.is_valid:
raise ValueError("empty or invalid geometry; run shapely.make_valid first")
return geom
whole -> the polygon wraps a pole; fill it in longitude sectors (see fill_in_sectors)
projected -> geometry is in EPSG:2056; reproject to EPSG:4326 first
projected, no crs -> bounds (2485468.010442513, 1075551.7941011817, 2831994.8583251485, 1295112.798875499) are not degrees - is this a projected CRS?
degrees passed
The first line is Antarctica, the last Switzerland in EPSG:4326.
The pole test compares against 89.9ยฐ, not 90ยฐ. Natural Earth's Antarctica reaches โ89.99999999999994ยฐ, and a first version that tested miny <= -90 let it straight through.
Example 2 โ rescue only the parts that came back empty
import h3
def fill_every_part(geom, res):
"""Centre containment for large parts, overlap for parts too small to hold a centre."""
parts = list(geom.geoms) if geom.geom_type == "MultiPolygon" else [geom]
cells, rescued = set(), 0
for part in parts:
found = h3.geo_to_cells(part, res)
if not found:
found = h3.h3shape_to_cells_experimental(h3.geo_to_h3shape(part), res, contain="overlap")
rescued += 1
cells.update(found)
print(f"res {res}: {len(parts)} parts, {rescued} rescued with overlap, {len(cells):,} cells")
return cells
res 5: 264 parts, 136 rescued with overlap, 7,356 cells
Indonesia: geo_to_cells 7,037, fill_every_part 7,356, added 319 (0.12s); overlap on whole 9,036
res 6: 74 parts, 17 rescued with overlap, 3,226 cells
Greece: geo_to_cells 3,173, fill_every_part 3,226, added 53 (0.05s); overlap on whole 4,128
Every island is represented, and the mainland keeps centre containment's area accuracy. Filling per part costs little: 0.12 s for Indonesia.
Example 3 โ fill a polar polygon in sectors
import h3
from shapely.geometry import box
def fill_in_sectors(geom, res, width=90):
"""Fill a polygon that wraps a pole by cutting it into longitude sectors first."""
cells, failed = set(), 0
for west in range(-180, 180, width):
piece = geom.intersection(box(west, -90, west + width, 90))
if piece.is_empty:
continue
try:
cells.update(h3.geo_to_cells(piece, res))
except h3.H3BaseException:
failed += 1
area = sum(h3.cell_area(c, "km^2") for c in cells)
print(f"res {res}: {len(cells):,} cells from {360 // width} sectors, {failed} failed, {area / 1e6:.2f} M km2")
return cells
res 4: 6,431 cells from 4 sectors, 0 failed, 12.08 M km2
res 5: 44,940 cells from 4 sectors, 0 failed, 12.06 M km2
res 6: 314,558 cells from 4 sectors, 0 failed, 12.06 M km2
At resolution 4, where the whole polygon also worked, the sectors produced the same 6,431 cells, so cutting changes nothing where no cut is needed. The failed counter makes a sector that still fails visible instead of silently thinning the result.
Explanation
Why centre containment drops things
A cell either has its centre inside a polygon or it does not, so a polygon narrower than a cell can fall entirely between centres. The rule is chosen for coverages: adjacent polygons must not both claim a cell. Overlap containment breaks that promise, which is why it covered 111% of Greece at resolution 7 and 622% of Malta at resolution 5.
Why a projected polygon gives zero cells instead of an error
The fill does not know which CRS the numbers came from. Coordinates in metres are read as enormous angles, and the resulting shape contains no cell centres, so the answer is an empty set โ a legitimate answer for a legitimate polygon, as far as H3 can tell. Only a bounds check before the call can distinguish "no cells" from "wrong units".
Why a ring around a pole fails
H3 treats a polygon as loops of vertices joined by great-circle arcs on the sphere. In the plane, Antarctica's ring is a tidy rectangle-bottomed shape: up the โ180ยฐ meridian, along the coast, down 180ยฐ and back along the bottom edge of the map. On the sphere, that bottom edge is not an edge at all. The measured polygon has 724 vertices at latitude โ89.99999999999994ยฐ, half a degree of longitude apart โ all of them within micrometres of the South Pole. The loop runs into a single point and out again hundreds of times, and from resolution 5 up the fill could not resolve it and raised H3FailedError.
Clipping at โ89.99ยฐ replaced those vertices with one edge from โ180ยฐ to 180ยฐ along the clip line. On the sphere those two endpoints are the same place, so the loop no longer encloses the cap the way the flat drawing suggests. The fill completed and quietly covered about half of the continent. Cutting into sectors avoids the problem altogether: no piece surrounds the pole, so every piece is an ordinary polygon on the sphere.
Why the number of cells is not a check
Every failure above except the exception still returns a set of plausible size. Indonesia's centre-containment fill covered 100.5% of the country's area while missing 136 islands, because the extra cells along the big coastlines balanced the missing ones. Compare the cells against the polygon part by part, or at least count the parts that received nothing.
Edge cases or notes
- Invalid geometry was not the usual cause. A self-intersecting bowtie filled 1,597 cells both before and after
make_valid; still repair invalid input before filling. - Antimeridian-crossing countries are fine when the polygon is already split at 180ยฐ: Russia filled 64,726 cells at resolution 5 in 2.9 s.
- Resolution drives fill time steeply. Filling all 258 countries took 12 s at resolution 5 and 414 s at resolution 7.
fullcontainment is the strictest. It never includes a cell that crosses the edge, so small or thin polygons often get nothing.bbox_overlapis a fast superset for pre-filtering, not a fill.- The empty
H3FailedErrormessage makes logs useless; catchh3.H3BaseExceptionand log the polygon's bounds yourself. - Very fine resolutions on large countries are slow and huge; compact the result if you need to keep it.
Internal links
- How to fill a polygon with H3 cells โ the containment modes in normal use
- Fixing H3 cells in the wrong place: latitude and longitude order โ the fill that succeeds somewhere else
- Fixing H3 hexagons that stretch across the map at the antimeridian โ the other trouble near 180ยฐ
- How to compact an H3 cell set to store a region cheaply โ keeping a large fill small
- Choosing an H3 resolution: cell size, counts and what each level can show โ matching cell size to the smallest polygon
- Fixing H3 AttributeError after upgrading: the v3 to v4 API rename โ polyfill's old coordinate trap
- How to fix invalid geometries in Python (GeoPandas) โ repairing input before the fill
- How to reproject spatial data in Python (GeoPandas) โ getting to EPSG:4326 first
FAQ
Why does h3 geo_to_cells return an empty list?
Usually because no cell centre falls inside the polygon: it is smaller than a cell at that resolution. The other common cause is a projected CRS, which also returns an empty result with no error.
How do I make sure small islands get a cell?
Fill each part separately and fall back to overlap containment for parts that return nothing. That added 319 cells for Indonesia's 136 empty islands without inflating the mainland.
What does H3FailedError with no message mean?
In practice, a polygon H3 cannot resolve, most often one that surrounds a pole. Antarctica's mainland raised it from resolution 5 upwards; filling four 90ยฐ longitude sectors worked.
Why does polygon_to_cells reject my shapely polygon?
It only accepts H3's LatLngPoly shapes. Pass shapely geometries and GeoJSON-like mappings to geo_to_cells instead.
Should I use overlap containment for everything?
No. It counts boundary cells for every polygon they touch, so a coverage double-counts and small shapes inflate. Greece covered 111% of its area at resolution 7, and Malta 622% at resolution 5.
Can I just clip the pole off the polygon?
Not safely. Clipping Antarctica at โ89.99ยฐ stopped the error but returned 23,515 cells instead of 44,940, with no warning. Fill in longitude sectors instead.