Purpose
Many execution platforms — DSPs, walled-garden ad products, DOOH networks,
loyalty systems — do not accept geometry at all. They accept a platform_geo_id:
a proprietary integer, hash, or code that references a boundary the platform
holds internally and does not expose. This page covers the adapter pipeline
that turns a canonical H3 cell set into the best available set of those IDs,
and the bookkeeping required because that translation is lossy and platform
specific by construction.
Source geometry and destination geometry
Source geometry is an h3_cell_set — a set of H3 cells at a stated
resolution, normalized to EPSG:4326. Destination geometry is
platform_geo_id: not a geometry at all, but an identifier drawn from a
platform's own geography namespace (a DMA-like market code, a proprietary
"zone" id, a hashed geofence id). The distinction matters enough to restate
plainly: an identifier is not a geometry. Two platforms can both expose an
id labeled "zone_4471" and mean entirely different polygons; an id carries
no shape, area, or boundary information on its own, only a lookup key into a
boundary set the platform controls and can change without notice.
Exactness class
This conversion is approximate and platform dependent: the achievable accuracy is capped by whatever boundary set the platform publishes or licenses, which is frequently coarser than the H3 resolution being converted from, and is never guaranteed to be a clean partition of the platform's own serving area. A platform's boundaries can also be entirely undocumented, in which case the mapping is built empirically (see platform-native-ids-only below) and carries lower confidence than a mapping built from a published boundary file.
Adapter pipeline
flowchart LR H["H3 cell set<br/>(source resolution)"] --> U["Normalized polygon union<br/>(dissolve, repair, EPSG:4326)"] U --> X["Versioned platform<br/>geography crosswalk"] X --> P["Platform-native IDs<br/>(+ confidence, unmatched cells)"]
The H3 cells are first dissolved into a single normalized polygon union — not kept as discrete cells — because most platform boundary sets are themselves polygons, and polygon-to-polygon overlap is the only reliable basis for matching against an opaque, externally defined geography. That union is then run against a versioned crosswalk built specifically for that platform's boundary vintage, which resolves to zero, one, or several platform-native IDs depending on how the union overlaps the platform's own zones.
Containment rule and boundary behavior
A platform id is emitted for a cell (or, after dissolving, for a portion of
the union) when the union's overlap with that platform zone exceeds a stated
match_threshold — commonly a coverage-ratio cutoff such as 0.5, meaning the
platform zone must account for at least half of the area under consideration
before its id is included. Coverage below the threshold is recorded as a
partial match, not silently dropped: it is retained with its actual
overlap fraction so a caller can decide whether to include it. A cell (or
union fragment) with no platform zone above any threshold is an unmatched
cell, and must appear in the output as unmatched rather than be omitted,
since omission is indistinguishable from "matched with zero weight" to a
downstream reader.
Resolution behavior
Higher H3 resolution improves the fidelity of the union that gets matched against platform zones, but does not improve the platform side of the match — the platform's own boundary vintage is the binding constraint on achievable accuracy. Increasing source resolution beyond the point where the union already tracks the intended area tightly yields no further improvement in match quality, only more cells to dissolve.
Units and CRS
Both the H3-derived union and the platform boundary set are normalized to EPSG:4326 before intersection; match thresholds are computed on spherical (haversine-consistent) m² area, consistent with the area conventions used elsewhere in this knowledge base.
Algorithm
import { normalizePolygon } from "@/lib/geometry/normalize";
import { weightedCrosswalk } from "@/lib/h3/crosswalk";
// 1. Dissolve the H3 cell set into a single normalized polygon union.
const union = normalizePolygon(dissolveCellsToPolygon(cells));
// 2. Match against a versioned platform geography crosswalk.
const matches = weightedCrosswalk([union], platformZonePolygons, {
weightBy: "area",
});
// 3. Keep matches above threshold; carry the rest as partial/unmatched.
const platformIds = matches.filter((m) => m.cell_coverage_fraction >= 0.5);
const partialMatches = matches.filter(
(m) => m.cell_coverage_fraction > 0 && m.cell_coverage_fraction < 0.5
);
The same conversion with the Python bindings (h3-py v4):
import h3
from shapely.geometry import Polygon, MultiPolygon
from shapely.ops import unary_union
def cells_to_union(cells) -> Polygon | MultiPolygon:
# h3-py's own dissolve: a cell set -> one (multi)polygon boundary.
# cells_to_h3shape returns a LatLngMultiPoly; walk its polygons/rings
# and flip each (lat, lng) vertex to shapely's (x, y) = (lng, lat).
# (Attribute names below are illustrative — consult the h3-py shape
# API for the exact accessor on the returned LatLngMultiPoly.)
shape = h3.cells_to_h3shape(cells, tight=True)
rings = [
Polygon(
[(lng, lat) for lat, lng in poly.outer],
[[(lng, lat) for lat, lng in hole] for hole in poly.holes],
)
for poly in shape.polygons
]
return unary_union(rings)
def crosswalk_to_platform(
union: Polygon | MultiPolygon,
platform_zones: dict[str, Polygon],
match_threshold: float = 0.5,
boundary_vintage: str = "unknown",
):
# Pseudo-crosswalk: match the dissolved union against a platform's own
# (externally sourced) zone polygons and bucket by coverage fraction.
matched, partial = [], []
covered = None
for platform_id, zone_poly in platform_zones.items():
overlap = union.intersection(zone_poly)
if overlap.is_empty:
continue
coverage_fraction = overlap.area / union.area
record = {
"platform_geo_id": platform_id,
"match_confidence": coverage_fraction,
"boundary_vintage": boundary_vintage,
}
covered = overlap if covered is None else unary_union([covered, overlap])
(matched if coverage_fraction >= match_threshold else partial).append(record)
unmatched_area = union.area - (covered.area if covered is not None else 0.0)
return {
"matched": matched,
"partial_matches": partial,
"unmatched_fraction": unmatched_area / union.area,
}
As with the crosswalk page above, shapely areas here are planar and only
illustrative; the tested reference implementation in this knowledge base is
the TypeScript in lib/, which computes match thresholds on spherical
(haversine-consistent) area and records boundary_vintage and
unmatched_cells explicitly rather than deriving them ad hoc.
Parameters
Match threshold, the platform's boundary vintage and ID namespace, and whether unmatched or partial-match cells should be surfaced to the caller or suppressed at delivery time (they should never be suppressed silently at the crosswalk-building step).
Outputs
A record per H3 union fragment: the resolved platform_geo_id (or explicit
unmatched), match_confidence (derived from cell_coverage_fraction),
the platform's id_namespace, the boundary_vintage of the platform file
used, and a list of unmatched_cells and partial_matches that did not clear
the threshold — this list is the primary artifact a media planner needs
before claiming full coverage.
Quality metrics
coverage_ratio and jaccard, computed between the source H3 union and the
union of all matched platform zones, quantify how much of the requested
geography the platform can actually express. A low coverage_ratio with a
long unmatched_cells list is a signal to renegotiate the requested geometry
or accept a documented shortfall, not a bug in the crosswalk itself.
Edge cases
Some platforms expose platform-native-ids-only —
no boundary file at all, only a picklist of ids with human-readable labels
("Downtown", "Zone 7"). In that case the crosswalk must be built empirically,
by observing where the platform actually delivers against a known test
geometry, and every such mapping should be flagged with materially lower
confidence than one built from a licensed boundary file. Stale boundaries
are a recurring failure mode here as well: platforms revise their internal
zones without a corresponding version bump on their public documentation, so
a crosswalk's boundary_vintage should be re-validated on a fixed schedule,
not assumed durable once built.
Assumptions and limitations
This conversion assumes the platform's boundary set is knowable at all
(published, licensed, or empirically reconstructable) and that its zones are
static for the duration the crosswalk is in use — an assumption platforms do
not always honor. When neither holds, the honest output is a documented
unmatched result rather than a best-effort id chosen without a stated
confidence.
