Purpose
A crosswalk answers a different question than the partition built in administrative polygon to H3: not "which single region owns this cell" but "which regions does this cell touch, and by how much." It exists for downstream uses that need to split a quantity — population, spend, audience, screen inventory — across regions in proportion to real overlap, rather than force each cell into one bucket.
Source geometry and destination geometry
Source geometry is an h3_cell_set, normalized to EPSG:4326. Destination
geometry is one or more administrative region types — admin_county,
census_geo (tract or block group), or dma — each a polygon or
multipolygon keyed by a stable region id (FIPS, GEOID, DMA code).
Exactness class
This conversion is weighted: it does not resolve to one right answer per cell, but a distribution of a cell's membership across every region it geometrically intersects, expressed as fractional weights. Two crosswalks built from the same cells and region polygons but a different weighting basis (raw area versus population) legitimately assign different shares of the same cell to the same region, and both are correct for their stated basis.
Containment rule and boundary behavior — membership rules
- centroid
- Cell belongs to the region whose polygon contains the cell center. Single-owner, cheap, blind to how much of the cell lies outside that region.
- largest-overlap
- Cell assigned to whichever region holds the largest share of its area. Single-owner, area-aware rather than point-aware.
- any-overlap
- Cell listed against every region it intersects at all, no fraction attached. Many-to-many, boolean — eligibility, not apportionment.
- full-containment
- Cell listed against a region only if the whole cell lies inside it. Straddling cells belong to no region alone; pair with a partial rule to avoid gaps.
- area-weighted
- Every (cell, region) pair retained, weight equal to intersection_area / cell area. Assumes uniform area density inside the cell.
- population-weighted
- Weight is region_population times (intersection_area / region_total_area). Assumes uniform population density, false near urban cores at fine resolution.
- audience-weighted
- Same interpolation, apportioning a platform or panel audience count instead of census population — subject to the panel's own coverage bias.
- inventory-weighted
- Weight is a count of physical or media units (screens, store fronts) intersecting the cell, apportioned by area or unit point locations.
- probabilistic
- Weight drawn from an exposure or gravity model instead of assumed-uniform area — road density, measured foot traffic. Only as good as that model's validation.
Why the many-to-many relationship must be preserved
A cell that straddles two regions is not an edge case to be resolved away —
it is the geometric reality of overlaying a hexagonal grid on polygons whose
boundaries were drawn without regard to that grid. Collapsing a straddling
cell to a single argmax region discards the minority share entirely.
Consider a cell split 70/30 between County A and County B: an argmax
crosswalk assigns 100% of the cell to County A, which is now overcounted
by the 30% it never held, while County B is undercounted by the 30% it
did hold — in the same operation, on the same cell. Aggregated over every
boundary cell along a county line, this is not noise but a systematic bias:
smaller regions sharing a long boundary with a larger neighbor lose share
every time, in the same direction. A many-to-many crosswalk is the only
representation that lets a caller reconstruct the true split later — a
single-owner table cannot be repaired downstream once the discarded fraction
is gone.
Resolution behavior
Coarser resolutions have fewer, larger cells, so a higher fraction of the cells adjacent to any boundary straddle it. Finer resolutions reduce that fraction — cell area shrinks roughly sevenfold per step while boundary- adjacent cell count grows only with the boundary's length — but the fraction never reaches zero at any finite resolution, since a boundary is a continuous curve and the grid is discrete. A crosswalk is required at every resolution; what changes is the total misallocated area a single-owner rule would incur, not whether the problem exists.
Units and CRS
Region polygons and H3 cell boundaries are normalized to EPSG:4326 before
intersection. intersection_area is computed as spherical (haversine-
consistent) m², matching cellArea(cell, "m2"), so cell_coverage_fraction
and region_coverage_fraction stay dimensionless ratios of comparable area
units rather than a mix of projected and unprojected area.
Algorithm
import { weightedCrosswalk, maxOverlapAssignment } from "@/lib/h3/crosswalk";
// Many-to-many: every (cell, region) pair the cell actually touches,
// retained with an area-based weight.
const crosswalk = weightedCrosswalk(cells, regionPolygons, {
resolution: 8,
weightBy: "area", // or "population" | "audience" | "inventory"
});
// Single-owner reference view, DERIVED from the same overlaps —
// never treat this as the source of truth for apportionment.
const primaryRegion = maxOverlapAssignment(cells, regionPolygons, {
resolution: 8,
});
The same conversion with the Python bindings (h3-py v4):
import h3
from collections import defaultdict
from shapely.geometry import Polygon
def cell_polygon(cell: str) -> Polygon:
# h3-py returns (lat, lng) pairs; shapely expects (x, y) = (lng, lat).
boundary = h3.cell_to_boundary(cell)
return Polygon([(lng, lat) for lat, lng in boundary])
def weighted_crosswalk(cells, region_polygons: dict[str, Polygon]):
# Many-to-many: every (cell, region) pair the cell actually touches,
# retained with an area-based weight.
rows = []
for cell in cells:
cell_poly = cell_polygon(cell)
cell_area = cell_poly.area
for region_id, region_poly in region_polygons.items():
overlap = cell_poly.intersection(region_poly).area
if overlap > 0:
rows.append({
"cell_id": cell,
"region_id": region_id,
"cell_coverage_fraction": overlap / cell_area,
})
return rows
def max_overlap_assignment(cells, region_polygons: dict[str, Polygon]):
# Single-owner reference view, DERIVED from the same overlaps — an
# argmax over each cell's rows, not a separate source of truth.
best = defaultdict(lambda: (None, 0.0))
for row in weighted_crosswalk(cells, region_polygons):
cell, region, frac = row["cell_id"], row["region_id"], row["cell_coverage_fraction"]
if frac > best[cell][1]:
best[cell] = (region, frac)
return {cell: region for cell, (region, _) in best.items()}
shapely here computes planar area on lat/lng coordinates, which is only
adequate for illustration at this scale — the tested reference
implementation in this knowledge base is the TypeScript in lib/, which
uses spherical (haversine-consistent) area throughout.
Parameters
Resolution, weighting basis (area, population, audience, inventory,
or a supplied probabilistic model), region polygon vintage, and whether
full-containment rows are emitted alongside partial rows.
Outputs
A weighted crosswalk table: cell_id, source_region_id,
intersection_area, cell_coverage_fraction (intersection_area divided by
cell area), region_coverage_fraction (intersection_area divided by region
area), and, when a non-area basis is requested, an added weight column
(population_weight, audience_weight, inventory_weight, or
probability_weight). A cell touching three regions produces three rows.
Quality metrics
For each cell, the sum of cell_coverage_fraction across its rows should
equal 1.0 within floating-point tolerance (typically 1e-6) if the region set
is a true partition; a sum below 1.0 indicates a gap under the cell, and a
sum above 1.0 indicates overlapping region polygons — both are per-cell
diagnostics worth surfacing before the crosswalk ships. Compute
coverage_ratio, overreach_ratio, and jaccard per region against its
source polygon to confirm the crosswalk's apportioned area tracks the
region's true area.
Edge cases
Duplicated region ids occur when a multipart
region (an island county, a DMA split by a lake) is stored as multiple ring
records sharing one id — deduplicating on region id before intersecting
drops coverage from the smaller part. Stale boundaries
are the more common failure: DMA and census vintages change year over year,
so a crosswalk built against last year's polygons misapportions every cell
near a moved boundary with no error raised — boundary_vintage must be
recorded and checked against what the destination expects.
Assumptions and limitations
Area- and population-weighted crosswalks assume uniform density within the
source region for whatever is being apportioned; this degrades visibly at
coarse resolution near dense urban boundaries, where population is not
remotely uniform across a county. When better information exists — a
gravity model, foot-traffic panel, inventory point locations — prefer
probabilistic or point-apportioned inventory-weighted over an area
default.
