Purpose
Administrative and statistical boundaries — counties, states, DMAs, census tracts, ZCTAs, and postal-code areas — are usually consumed as a partition: every H3 cell should belong to exactly one region, because reporting, budgeting, and compliance logic downstream assumes non-overlapping buckets. This page covers the conversion path that preserves that property, and contrasts it with the retention-oriented crosswalk that does not.
Source geometry and destination geometry
Source geometry is one of admin_county, admin_state, dma, census_geo,
or postal_code — each supplied as a polygon or multipolygon in a vendor
shapefile, GeoJSON file, or database geometry column, keyed by an identifier
(FIPS, GEOID, DMA code, ZIP/ZCTA). Destination geometry is an
h3_cell_set: a set of H3 cells at a stated resolution, each tagged with
exactly one region id.
Exactness class
This conversion is approximate: no assignment rule reproduces the source polygon's area exactly, and the boundary of the assigned cell set will not coincide with the source boundary at any resolution short of the coordinate precision of the original survey. Two different, valid assignment rules (below) produce two different cell sets from the same input.
Containment rule and boundary behavior
Two assignment rules are in scope, and they answer different questions:
- Center-contained (partition)
- A cell belongs to region R if and only if the cell's center point falls inside R's polygon. Every cell is assigned to at most one region by construction, so the resulting cell set is a true partition — the property callers usually want from admin boundaries.
- Max-overlap assignment (crosswalk)
- A cell that straddles two or more regions is assigned to whichever region contains the largest share of the cell's area. Used when a cell must be labeled but only touches a region's edge — this also yields a partition, but by area majority rather than center.
Center-contained is the default for reporting-grade partitions because it is deterministic and reproducible from the polygon and the H3 grid alone, without needing an area computation per cell. Max-overlap is used when the polygon boundary runs close to many cell centers (common at res 8+ near jagged county lines) and center-containment would otherwise assign a disproportionate number of boundary cells to whichever side of the line the grid happens to bias toward.
Neither rule should be confused with the weighted crosswalk,
which deliberately breaks the partition property: it retains every
(cell_id, region_id) pair a cell touches, with an intersection-area
fraction per pair, so that population or spend can be split proportionally
across regions instead of forced into one.
Resolution behavior
Higher resolution cells track the source boundary more closely because cell area shrinks roughly sevenfold per resolution step, shrinking the maximum possible per-cell disagreement between center-containment and the true polygon edge. At res 6, a single mis-assigned boundary cell can misplace several square kilometers; at res 9, the same error is bounded to a few hectares. Multipart admin units (a county with an offshore island, a DMA split by a lake) need per-part polyfilling — polyfilling the multipolygon as a whole can silently drop small parts if the polyfill implementation does not iterate rings.
Units and CRS
Source polygons must be normalized to EPSG:4326 before polyfilling; areas for overlap computation are computed as spherical (haversine-consistent) m². Vendor shapefiles delivered in a projected CRS (state plane, Albers) must be reprojected first — reprojection error is typically under 1 meter for CONUS-scale admin polygons but should be checked, not assumed, for Alaska, Hawaii, and territories.
Algorithm
import { polygonToH3 } from "@/lib/h3/polyfill";
import { maxOverlapAssignment } from "@/lib/h3/crosswalk";
// Partition: center-contained, one region per cell
const partitionCells = polygonToH3(countyPolygon, {
resolution: 8,
mode: "center",
});
// Partition by area majority, for boundary-heavy geographies
const majorityCells = maxOverlapAssignment(candidateCells, regionPolygons, {
resolution: 8,
});
The same conversion with the Python bindings (h3-py v4):
import h3
from shapely.geometry import Polygon
# Center-contained partition: one polygon_to_cells call per region
region_cells = {
region_id: h3.polygon_to_cells(h3.LatLngPoly(ring), res=8)
for region_id, ring in region_rings.items() # ring = [(lat, lng), ...]
}
# Max-overlap assignment for cells straddling more than one region
region_shapely = {
region_id: Polygon([(lng, lat) for lat, lng in ring])
for region_id, ring in region_rings.items()
}
candidate_cells = set().union(*region_cells.values())
assignment = {}
for cell in candidate_cells:
boundary = Polygon([(lng, lat) for lat, lng in h3.cell_to_boundary(cell)])
best_region, best_area = None, 0.0
for region_id, poly in region_shapely.items():
area = boundary.intersection(poly).area
if area > best_area:
best_region, best_area = region_id, area
assignment[cell] = best_region # argmax over intersection area
The tested reference implementation for this conversion is the TypeScript
in lib/; h3-py has no built-in maxOverlapAssignment, so the Python
above reproduces the same argmax-over-intersection-area loop with
shapely.
Parameters
Resolution (int, typically 7–9 for county/DMA-scale work), assignment mode
(center or max-overlap), boundary vintage (the effective date of the
source file), and the region id field to preserve.
Outputs
A cell-to-region table: cell_id, region_id, resolution,
assignment_mode, boundary_vintage. No overlap fraction is stored, because
by construction each cell has exactly one region.
Quality metrics
Compute coverage_ratio and overreach_ratio per region against the source
polygon; a well-behaved center-contained partition typically shows
coverage_ratio in the 0.90–0.98 range with overreach_ratio near zero,
since center-containment cannot assign a cell whose center lies outside the
polygon. Check jaccard per region as a single combined figure for boundary
tightness across resolutions.
Edge cases
FIPS and GEOID codes are numeric strings with meaningful leading zeros
("01001" for Alabama, Autauga County); casting them to integers during a
join silently corrupts the key — this is the single most common cause of
"missing counties" bugs. Boundary vintage matters: county lines are stable,
but DMA boundaries and ZCTA definitions change year over year
(stale-boundaries), so a cell-to-region table
built from a 2019 DMA file will misclassify cells near any boundary that
moved since. Some vendor files carry
duplicated-region-ids — the same GEOID appearing
on two disjoint ring records for a single county, which is legal
multipart geometry, not a duplicate to be deduplicated away. And a ZIP code
is not a polygon: it is a USPS delivery-route abstraction with no
authoritative boundary; any "ZIP polygon" in circulation is a third party's
ZCTA approximation and should be labeled and sourced as such rather than
treated as ground truth.
Assumptions and limitations
This conversion assumes the source file is a genuine partition of its parent geography (no gaps, no overlaps) before polyfilling — polyfilling cannot repair a source file that already double-counts territory. It also assumes callers need a partition; if the actual need is proportional weighting across regions, use the arbitrary polygon threshold/weighted path instead of forcing a single-region assignment.
