All sections

Conversion Quality Metrics

Coverage, overreach, underreach, and Jaccard share the same numerator family but different denominators, so a geometry can score high on one and poorly on another simultaneously.

exactstableh36 min read

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=area(sourceexecution)area(source)\text{coverage\_ratio} = \frac{\text{area}(\text{source} \cap \text{execution})}{\text{area}(\text{source})}

overreach_ratio=area(executionsource)area(source)\text{overreach\_ratio} = \frac{\text{area}(\text{execution} - \text{source})}{\text{area}(\text{source})}

underreach_ratio=area(sourceexecution)area(source)\text{underreach\_ratio} = \frac{\text{area}(\text{source} - \text{execution})}{\text{area}(\text{source})}

jaccard=area(sourceexecution)area(sourceexecution)\text{jaccard} = \frac{\text{area}(\text{source} \cap \text{execution})}{\text{area}(\text{source} \cup \text{execution})}

High coverage and high overreach can both be true

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.

full: coverage 49.4%, overreach 0%
full: coverage 49.4%, overreach 0%
intersect: coverage 100%, overreach 68%
intersect: coverage 100%, overreach 68%

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.

Illustration — duplicate eligibility

Two adjacent cells executed as circumscribed circles: the pink lens is ground eligible under both targets — the duplicate-eligibility area.
Rendered from the tested conversion code · Two adjacent cells executed as circumscribed circles: the pink lens is ground eligible under both targets — the duplicate-eligibility area.
Edge cases affecting this page
  • - Polygons much smaller than a cell may be missed or over-represented by a single cell.
  • - A cell that only shares a boundary point/edge (zero area) with the polygon.