Fixing H3 Polygon Fill That Misses Cells or Returns Nothing

On this page

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.

Five H3 polygon fill failures and the fix for each. empty, short or failed fillssymptomfixsmall polygon returns []finer resolution, or overlapcontainmentislands missing from a countryfill per part, rescue the emptyparts[] with no error at any resreproject to EPSG:4326 firstValueError: Unrecognized typeshapely and GeoJSON go togeo_to_cellsH3FailedError, empty messagepolygon wraps a pole: fill insectorsOnly the last two raise; the first three look like results spatialworkflow.io
Three of the five return an empty or short result with no error, so the check has to come from you.

Centre rule, small polygons, lost parts, projected input

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,779  (111.1%)  20,036  (89.7%)
Switzerland     7     8,317  ( 99.7%)   8,709  (104.4%)   7,927  (95.0%)
Malta           5         2  (177.9%)       6  (533.5%)       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 8,807, 126% 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

What a fill does with projected coordinates, and what changes after reprojecting. projected coordinates fill nothing, silentlySwiss LV95after to_crs(4326)coordinates2,485,468 / 1,075,552✓8.23 / 46.82bounds✕far outside ±180, ±90✓insidegeo_to_cells gave✕0 cells, and no error✓the cellsCheck the bounds are within ±180 and ±90 before filling — nothing else will tell you spatialworkflow.io
H3 reads any finite pair as degrees, so it has nothing to complain about.

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.

Share of polygon parts that received no H3 cell under centre containment in four countries. polygon parts with no cell (centre containment)Philippines, res 551 of 97 partsIndonesia, res 5136 of 264 · 10,236 km²Greece, res 617 of 74 partsNorway, res 68 of 120 parts060%Indonesia's fill still covered 100.5% of its area spatialworkflow.io
Archipelagos lose half their parts at resolution 5, and the total cell count still looks right.

Rejecting silent failures and rescuing empty parts

The data used on this page

Country outlines from Natural Earth's 1:10m admin-0 layer. GeoNames is edited daily, so counts drift a little between downloads.

import os
import zipfile

import pandas as pd
import requests


def fetch(url, path):
    if not os.path.exists(path):
        with requests.get(url, stream=True, timeout=600) as reply:
            reply.raise_for_status()
            with open(path, "wb") as out:
                for chunk in reply.iter_content(1 << 20):
                    out.write(chunk)


fetch("https://naciscdn.org/naturalearth/10m/cultural/ne_10m_admin_0_countries.zip",
      "ne_10m_admin_0_countries.zip")
print("ready")
ready

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


import geopandas as gpd

world = gpd.read_file("ne_10m_admin_0_countries.zip")
antarctica = max(world.loc[world.ADMIN == "Antarctica", "geometry"].iloc[0].geoms, key=lambda p: p.area)
swiss = gpd.GeoSeries(world.loc[world.ADMIN == "Switzerland", "geometry"], crs=world.crs)
for label, geom, crs in [("whole", antarctica, world.crs), ("projected", swiss.to_crs(2056).iloc[0], swiss.to_crs(2056).crs),
                         ("projected, no crs", swiss.to_crs(2056).iloc[0], None), ("degrees", swiss.iloc[0], world.crs)]:
    try:
        check_fill_input(geom, crs)
        print(label, "passed")
    except ValueError as exc:
        print(label, "->", exc)
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


import time

for name, res in [("Indonesia", 5), ("Greece", 6)]:
    geom = world.loc[world.ADMIN == name, "geometry"].iloc[0]
    start = time.perf_counter()
    every = fill_every_part(geom, res)
    took = time.perf_counter() - start
    plain = h3.geo_to_cells(geom, res)
    whole = h3.h3shape_to_cells_experimental(h3.geo_to_h3shape(geom), res, contain="overlap")
    print(f"  {name}: geo_to_cells {len(plain):,}, fill_every_part {len(every):,}, added {len(every) - len(plain)} "
          f"({took:.2f}s); overlap on whole {len(set(whole)):,}")
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.11s); overlap on whole 8,807
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.04s); overlap on whole 4,104

Every island is represented, and the mainland keeps centre containment's area accuracy. Filling per part costs little: about a tenth of a second 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


for res in (4, 5, 6):
    fill_in_sectors(antarctica, res)
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.

Example 4 — every table on this page, measured

import time

import geopandas as gpd
import h3
import shapely
from pyproj import Geod
from shapely.geometry import Polygon, box

geod = Geod(ellps="WGS84")
world = gpd.read_file("ne_10m_admin_0_countries.zip")
country = lambda name: world.loc[world.ADMIN == name, "geometry"].iloc[0]
km2 = lambda g: abs(geod.geometry_area_perimeter(g)[0]) / 1e6
cells_km2 = lambda cells: sum(h3.cell_area(c, "km^2") for c in cells)


def fill(geom, res, mode="center"):
    if mode == "center":
        return set(h3.geo_to_cells(geom, res))
    return set(h3.h3shape_to_cells_experimental(h3.geo_to_h3shape(geom), res, contain=mode))


for res in (3, 5, 7):
    empty, failed = [], []
    for name, g in zip(world.ADMIN, world.geometry):
        try:
            if not fill(g, res):
                empty.append(name)
        except h3.H3FailedError:
            failed.append(name)
    named = [n for n in ("Vatican", "Coral Sea Islands", "Scarborough Reef") if n in empty]
    print(f"res {res}: {len(empty)} of {len(world)} countries get no cell ({', '.join(named)} among them); "
          f"fill failed for {failed}")
for name, res in [("Greece", 7), ("Switzerland", 7), ("Malta", 5)]:
    g = country(name)
    print(name, res, "  ".join(f"{m} {len(c):,} ({cells_km2(c) / km2(g):.1%})"
                               for m in ("center", "overlap", "full") for c in [fill(g, res, m)]))
vatican = country("Vatican")
print(f"Vatican ({km2(vatican):.3f} km²)", {r: len(fill(vatican, r)) for r in (7, 8, 9)},
      "overlap at 7:", len(fill(vatican, 7, "overlap")))

for name, res in [("Indonesia", 5), ("Philippines", 5), ("Greece", 6), ("Norway", 6)]:
    parts = list(country(name).geoms)
    lost = [p for p in parts if not fill(p, res)]
    rescue = set().union(*(fill(p, res, "overlap") for p in lost)) - fill(country(name), res)
    print(f"{name} res {res}: {len(parts)} parts, {len(lost)} with no cell, {sum(km2(p) for p in lost):,.0f} km² lost, "
          f"{len(rescue)} cells to rescue them")
indonesia = country("Indonesia")
over = fill(indonesia, 5, "overlap")
print(f"Indonesia centre fill covers {cells_km2(fill(indonesia, 5)) / km2(indonesia):.1%} of its area; all-overlap: "
      f"{len(fill(indonesia, 5)):,} -> {len(over):,} cells, {cells_km2(over) / km2(indonesia):.0%}")
start = time.perf_counter()
print(f"Russia, already split at 180°: {len(fill(country('Russia'), 5)):,} cells in {time.perf_counter() - start:.1f} s")

lv95 = gpd.GeoSeries([country("Switzerland")], crs=4326).to_crs(2056).iloc[0]
print("Switzerland in LV95:", [round(v) for v in lv95.bounds[:2]], "->", len(fill(lv95, 5)), "cells")
for label, arg in [("shapely polygon", country("Switzerland")), ("GeoJSON dict", shapely.geometry.mapping(country("Switzerland"))),
                   ("LatLngPoly with 2 points", None)]:
    try:
        h3.polygon_to_cells(arg if arg is not None else h3.LatLngPoly([(0, 0), (1, 1)]), 5)
    except Exception as exc:
        print(f"{label}: {type(exc).__name__}: {exc}")

for res in range(0, 7):
    try:
        print(f"Antarctica res {res}: {len(fill(antarctica, res)):,} cells")
    except h3.H3FailedError as exc:
        print(f"Antarctica res {res}: H3FailedError({str(exc)!r})")
for cut in (-89.99, -86):
    try:
        print(f"clipped at {cut}: {len(fill(antarctica.intersection(box(-180, cut, 180, 90)), 5)):,} cells")
    except h3.H3FailedError:
        print(f"clipped at {cut}: H3FailedError")
for n in (4, 8):
    start = time.perf_counter()
    sectors = set().union(*(fill(antarctica.intersection(box(x, -90, x + 360 / n, 90)), 5)
                            for x in range(-180, 180, 360 // n)))
    print(f"{n} sectors: {len(sectors):,} cells, {cells_km2(sectors) / 1e6:.2f} M km² in {time.perf_counter() - start:.1f} s; "
          f"polygon {km2(antarctica) / 1e6:.2f} M km²")

swiss = country("Switzerland")
swapped = h3.polygon_to_cells(h3.LatLngPoly(list(swiss.exterior.coords)), 7)
print(f"Switzerland with (lng, lat) given as (lat, lng): {len(swapped):,} cells centred near "
      f"{[round(v, 1) for v in h3.cell_to_latlng(next(iter(swapped)))]}, against {len(fill(swiss, 7)):,}")
italy = max(country("Italy").geoms, key=lambda p: p.area)
with_holes, outer = fill(italy, 9), fill(Polygon(italy.exterior), 9)
san_marino = fill(country("San Marino"), 9)
print(f"Italy mainland: {len(italy.interiors)} holes; {len(outer - with_holes)} fewer cells with them; "
      f"San Marino's {len(san_marino)} cells all among them: {san_marino <= outer - with_holes}")
res 3: 87 of 258 countries get no cell (Vatican, Coral Sea Islands, Scarborough Reef among them); fill failed for []
res 5: 27 of 258 countries get no cell (Vatican, Coral Sea Islands, Scarborough Reef among them); fill failed for ['Antarctica']
res 7: 7 of 258 countries get no cell (Vatican, Coral Sea Islands, Scarborough Reef among them); fill failed for ['Antarctica']
Greece 7 center 22,303 (99.9%)  overlap 24,779 (111.1%)  full 20,036 (89.7%)
Switzerland 7 center 8,317 (99.7%)  overlap 8,709 (104.4%)  full 7,927 (95.0%)
Malta 5 center 2 (177.9%)  overlap 6 (533.5%)  full 0 (0.0%)
Vatican (0.012 km²) {7: 0, 8: 0, 9: 1} overlap at 7: 1
Indonesia res 5: 264 parts, 136 with no cell, 10,236 km² lost, 319 cells to rescue them
Philippines res 5: 97 parts, 51 with no cell, 3,826 km² lost, 127 cells to rescue them
Greece res 6: 74 parts, 17 with no cell, 280 km² lost, 53 cells to rescue them
Norway res 6: 120 parts, 8 with no cell, 141 km² lost, 25 cells to rescue them
Indonesia centre fill covers 100.5% of its area; all-overlap: 7,037 -> 8,807 cells, 126%
Russia, already split at 180°: 64,726 cells in 2.7 s
Switzerland in LV95: [2485468, 1075552] -> 0 cells
shapely polygon: ValueError: Unrecognized type: <class 'shapely.geometry.polygon.Polygon'>
GeoJSON dict: ValueError: Unrecognized type: <class 'dict'>
LatLngPoly with 2 points: ValueError: Non-empty LatLngPoly loops need at least 3 points.
Antarctica res 0: 3 cells
Antarctica res 1: 18 cells
Antarctica res 2: 130 cells
Antarctica res 3: 917 cells
Antarctica res 4: 6,431 cells
Antarctica res 5: H3FailedError('')
Antarctica res 6: H3FailedError('')
clipped at -89.99: 23,515 cells
clipped at -86: H3FailedError
4 sectors: 44,940 cells, 12.06 M km² in 0.4 s; polygon 12.16 M km²
8 sectors: 44,940 cells, 12.06 M km² in 0.3 s; polygon 12.16 M km²
Switzerland with (lng, lat) given as (lat, lng): 13,851 cells centred near [8.2, 47.3], against 8,317
Italy mainland: 2 holes; 548 fewer cells with them; San Marino's 547 cells all among them: True

Why centre containment drops things

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 534% 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.

Four ways to fill Antarctica's mainland at H3 resolution 5 and the result of each. Antarctica's mainland at resolution 5approachresultthe whole polygonH3FailedError (no message)clip at −89.99°23,515 cells, no errorclip at −86°H3FailedErrorfour 90° sectors44,940 cells · 12.06 M km²the polygon has 724 vertices within micrometres of the poleEight 45° sectors returned the same 44,940 cells spatialworkflow.io
The clip that silences the error is the worst of the four: no exception, and half the cells.

Valid geometry, date-line countries and fill time

  • Invalid geometry was not the cause of any failure here, but repair invalid input with make_valid before filling anyway.
  • Antimeridian-crossing countries are fine when the polygon is already split at 180°: Russia filled 64,726 cells at resolution 5 in 2.7 s.
  • Resolution drives fill time steeply. Each level multiplies the cell count by seven, and filling all 258 countries at resolution 7 took minutes rather than seconds.
  • full containment is the strictest. It never includes a cell that crosses the edge, so small or thin polygons often get nothing.
  • bbox_overlap is a fast superset for pre-filtering, not a fill.
  • The empty H3FailedError message makes logs useless; catch h3.H3BaseException and 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.

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 534% 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.