Every conversion in this knowledge base can be scored against its source geometry using a small, fixed set of area-based metrics. All areas below are spherical square meters computed on EPSG:4326 rings (turf's spherical area estimate); all metrics compare exactly two geometries at a time — a source (what was requested or normalized) and an execution (what will actually run, or did run).
Base quantities
- source area
- area(source) — the normalized source polygon's area, m2.
- execution area
- area(execution) — the executed geometry's area (circle, simplified polygon, cell-set outline), m2.
- intersection
- area(source cap execution) — ground both claim, m2.
- union
- area(source cup execution) — ground either claims, m2.
- uncovered_area
- area(source minus execution) — asked-for ground with no execution coverage, m2.
- duplicate_eligibility_area
- Sum of individual feature areas minus the area of their union, for a set of overlapping executed features (e.g. circles) — ground eligible under more than one target, m2.
Population, audience, and inventory covered are derived quantities, not
independent metrics: they are computed by applying a per-cell weight
(population density, audience count, inventory volume) to the intersection
area or the coverage fraction, exactly as the
weighted crosswalk does. They
inherit the same denominator caveats as coverage_ratio below and should
always be reported alongside the ratio, not instead of it.
The four ratios
Each ratio uses source area as the denominator except Jaccard, which uses
the union. Read the denominator before comparing two ratios across
different geometries.
coverage_ratio and overreach_ratio share a numerator family but are not
complementary — they do not sum to 1, and neither bounds the other.
Circumscribing every cell in a region produces coverage_ratio at or above
0.999 (the source is fully contained in the union of circles, by
construction) while overreach_ratio is strictly positive and can exceed 1
if the circles are large relative to the source polygon — meaning the
executed geometry is larger than the entire source, not merely imperfectly
aligned with it. Never report coverage_ratio alone as a proxy for
targeting precision; always pair it with overreach_ratio or jaccard.
underreach_ratio is the complement structure to watch instead: for a
single-source, single-execution comparison,
coverage_ratio + underreach_ratio = 1 always holds, because
area(source cap execution) + area(source - execution) = area(source) by
set-algebra identity regardless of what the execution geometry looks like.
overreach_ratio has no such fixed relationship to the other two because
its numerator is measured against execution, not source.
Boundary displacement, counts, and coverage denominators
Boundary displacement is a distance metric, not an area metric: the
maximum or mean perpendicular distance between the source boundary and the
nearest point on the execution boundary, in meters. It answers "how far did
the edge move," which overreach_ratio and underreach_ratio cannot answer
on their own — a geometry can have small overreach_ratio and still have a
boundary that moved considerably if the source polygon is large relative to
the displacement.
Counts (cells, targets, regions) are reported alongside area metrics but are not substitutes for them: cell count says nothing about coverage without knowing the resolution, and a small cell count at a coarse resolution can cover more area than a large cell count at a fine resolution. "Population/audience/inventory covered" figures must always be reported with the coverage_ratio and underreach_ratio that produced them, since a weighted total with no denominator context cannot be checked against the source ask.
Algorithm
import { coverageMetrics, duplicateEligibilityAreaM2 } from "@/lib/metrics";
import { cellToPolygon } from "@/lib/h3/polyfill";
import { circumscribedCircle } from "@/lib/h3/circles";
import * as turf from "@turf/turf";
// Coverage/overreach/underreach/jaccard for one cell approximated by its
// circumscribed circle.
const cell = "872830829ffffff";
const sourcePoly = cellToPolygon(cell);
const circle = circumscribedCircle(cell);
const executionPoly = turf.circle(
[circle.center[1], circle.center[0]],
circle.radiusMeters / 1000,
{ units: "kilometers", steps: 128 },
);
const m = coverageMetrics(sourcePoly, executionPoly);
// m.coverageRatio ~ 1 (circumscribed circle fully contains the cell)
// m.overreachRatio > 0 (the disk covers ground outside the hexagon)
// m.jaccardSimilarity < 1 (disk area exceeds cell area)
// Duplicate eligibility across two adjacent cells' circumscribed circles.
const neighborCircle = circumscribedCircle("872830828ffffff");
const neighborPoly = turf.circle(
[neighborCircle.center[1], neighborCircle.center[0]],
neighborCircle.radiusMeters / 1000,
{ units: "kilometers", steps: 128 },
);
const dupArea = duplicateEligibilityAreaM2([executionPoly, neighborPoly]);
// dupArea > 0: ground eligible under both circles.
The same conversion with the Python bindings (h3-py v4):
import h3
from shapely.geometry import Polygon
def cell_to_polygon(cell: str) -> Polygon:
boundary = h3.cell_to_boundary(cell)
return Polygon([(lng, lat) for lat, lng in boundary])
def coverage_metrics(source: Polygon, execution: Polygon) -> dict:
intersection = source.intersection(execution).area
union = source.union(execution).area
return {
"coverage_ratio": intersection / source.area,
"overreach_ratio": execution.difference(source).area / source.area,
"underreach_ratio": source.difference(execution).area / source.area,
"jaccard": intersection / union,
}
cell = "872830829ffffff"
source_poly = cell_to_polygon(cell)
# Executed geometry approximated by a circumscribed circle around the cell
# center — build it the same way the TS lib's circumscribedCircle does
# (great-circle radius, densified boundary), not with a planar buffer.
execution_poly = circumscribed_circle_polygon(cell)
m = coverage_metrics(source_poly, execution_poly)
# m["coverage_ratio"] ~ 1 (circle fully contains the hexagon)
# m["overreach_ratio"] > 0 (the disk covers ground outside the hexagon)
# m["jaccard"] < 1 (disk area exceeds cell area)
# Cell-count-free area: h3.cell_area never requires counting cells to size
# a region, unlike a count-times-nominal-area estimate.
exact_cell_area_m2 = h3.cell_area(cell, unit="m^2")
The tested reference implementation in this knowledge base is the
TypeScript in lib/; it computes every area above as spherical
(haversine-consistent) m², whereas shapely's .area on raw lat/lng
coordinates is planar and only adequate for a rough illustration at this
scale.
Reading the outputs together
A conversion report should never publish a single ratio in isolation.
coverage_ratio alone cannot distinguish a tightly-fit execution from a
grossly oversized one that happens to fully contain the source; pairing it
with overreach_ratio (or jaccard, which penalizes both under- and
over-coverage in one number) closes that gap.
duplicate_eligibility_area_m2 is the only metric here that requires more
than two geometries — it is defined over a set of executed features, and is
the correct diagnostic for "how much ground is double-counted," which
neither overreach_ratio nor jaccard computed pairwise can reveal, since
overlaps between two non-source features never appear in a source-vs-single-execution
comparison.
Edge cases
Tiny polygons (tiny-polygons) produce unstable
ratios when the source area approaches the numerical noise floor of the
area calculation — a source polygon a few square meters in extent can show
overreach_ratio in the hundreds or thousands purely because the
denominator is small, not because the execution is unusually bad; treat
extreme ratios on tiny sources as a signal to inspect absolute areas, not as
a literal severity score. Touching-only intersections
(touching-only) — where a boundary-adjacent
cell shares only an edge or point with the source, contributing
near-zero intersection area — should be filtered by an intersection-area
epsilon before computing coverage_ratio, or a geometrically-touching but
practically-irrelevant cell will be counted as "covering" the source.
