All sections

Administrative Polygon To H3

Converting counties, states, DMAs, census geographies, and postal areas into H3 cells while preserving the partition property those units are supposed to have.

approximatestableh36 min read
Source geometry
admin_county, admin_state, dma, census_geo, postal_code
Destination geometry
h3_cell_set

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.

Max-overlap partition of two adjacent regions at R8; each cell to its argmax region (blue=west, pink=east).
Rendered from the tested conversion code · Max-overlap partition of two adjacent regions at R8; each cell to its argmax region (blue=west, pink=east).

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.

Edge cases affecting this page
  • - Admin/postal/DMA boundaries change; using an old vintage misassigns cells.
  • - The same admin id mapping to multiple polygons (data error) breaks partition assumptions.
  • - Disjoint parts (islands, exclaves) must all be filled; a single-ring assumption drops parts.