Purpose
The dominant location signal on the open web is an IP geolocation lookup: a
center coordinate plus an accuracy radius at a stated confidence — commonly
the radius within which the vendor holds roughly 67% confidence the device
sits. At advertising-relevant resolutions that disk spans dozens to hundreds
of cells. The near-universal practice of collapsing the lookup to its center
and calling latLngToCell is a silent center-mode conversion applied to an
input that is not a point, and it discards exactly the information that makes
the signal usable: how much of the device's probability mass actually falls
inside any geography you care about.
This page defines the honest conversion: the lookup becomes a weighted cell
set — {cell, p} rows with probability mass in place of area fraction —
resolved from a versioned crosswalk (the vendor's database release is the
vintage). Both objects already exist elsewhere in this knowledge base; nothing
here is new machinery, only the refusal to pretend a disk is a point.
Source geometry and destination geometry
Source geometry is ip_block (a network block resolved through a geolocation
database, carrying accuracy_radius, confidence_convention, db_vintage,
and source) or a device_ping whose locationSource is IP rather than
GPS. Destination geometry is an h3_cell_set whose weights are probability
masses summing to at most 1, with the remainder carried explicitly as an
unlocated residual.
Exactness class
Approximate, at three layered points: the vendor's center estimate is frequently a registry or population centroid, not an observation — a correctly sized circle around a systematically displaced point; the declared radius is a model claim whose honesty is the vendor's calibration, not a measurement you can audit from the lookup alone; and the kernel spreading mass over the disk is itself a modeling choice. None of this is repaired by higher H3 resolution — see resolution behavior below.
The two required declarations before any mass is computed
Confidence convention. Vendors and SDKs do not agree on what a radius means. A radius at ~67% confidence (the 1σ-equivalent form IP vendors publish), a CEP50 median radius, and a raw Rayleigh σ differ by factors large enough that pooling them unconverted produces containment estimates wrong by a factor of two or more. The convention is mandatory metadata; normalize every radius to one confidence level before anything downstream touches it.
Database vintage. Geolocation databases release weekly. The release you
resolved against is a crosswalk vintage exactly like a boundary-file vintage:
it carries valid_from/valid_to, joins across a vintage gap are refused,
and — the sharpest consequence — the vintage used for experiment match
decisions is pinned for the assignment period, because a block whose
estimate crosses an arm boundary between releases flips every device behind
it between arms mid-flight (see the vintage-arm-flip edge case).
Vendors that publish confidence instead of a radius
Not every geolocation vendor declares uncertainty as a radius. The second
form in production is per-field confidence: a per-lookup probability
that the device is in a named country, region, city, or postal area, with
no radius at all. The same {cell, p} shape carries it — the declared
confidence spreads over the named region's cells through the standard
weighted crosswalk (area- or population-weighted, declared), and the
remainder is the same explicit unlocated residual the disk form carries.
Radius vendors and confidence vendors declare differently and interoperate
identically; every match rule, disposition, and contamination computation
below consumes the two without knowing which vendor shape produced them.
The reference implementation exposes this as region_confidence_to_cells
beside disk_to_cells.
Match rules: the four containment modes over probability mass
The four containment modes generalize from area measure to probability measure and become the match rules a delivery platform must declare:
| Area mode | Probabilistic match rule |
|---|---|
center | The point estimate falls in a declared cell. Today's silent default, now nameable and auditable. |
full | The entire declared-confidence disk lies inside the declared set. |
intersect | Any probability mass touches the declared set. |
threshold | P(true location ∈ declared set) ≥ p, with p declared. |
threshold is the workhorse. The unlocated residual (the mass outside the
declared-confidence disk) is never credited to a declared set under any rule,
and every match decision reports expected_overreach = 1 − in_set_mass —
matched or not — so probabilistic spill is a declared quantity rather than a
discovered one.
Resolution behavior
Resolution does not buy precision here; it only changes how many cells the disk smears across. A 1.2 km radius at res 8 spans a handful of cells; at res 10 it spans hundreds, each carrying a sliver of mass that no downstream consumer can act on. Cap the indexing resolution to the signal's own accuracy — a disk that would touch more cells than a sane enumeration bound is a resolution mismatch, not a bigger computation. The right failure mode is an error advising a coarser resolution, not a silent grind.
Units and CRS
Center in EPSG:4326 decimal degrees; radius in meters, normalized through the declared convention before use; mass dimensionless in [0, 1]. All disk and cell areas geodesic, in square metres — a probability computed as a ratio of planar degree-areas drifts with latitude.
Algorithm
// Uniform kernel: declared mass spread over the confidence disk;
// the tail (1 - confidence) is an explicit unlocated residual.
import { ipDiskToCells } from "@/lib/h3/ip";
const dist = ipDiskToCells(lat, lng, radiusM, {
resolution: 8,
confidence: 0.67,
convention: "confidence", // or "cep50" | "one_sigma" — normalized first
});
// dist.cells: [{ cell, p }], sum(p) ≈ 0.67; dist.residualMass ≈ 0.33
const inSetMass = dist.cells
.filter(({ cell }) => declaredSet.has(cell))
.reduce((s, { p }) => s + p, 0);
const matched = inSetMass >= 0.6; // threshold rule, p declared
The same conversion in Python (mirroring the tested reference implementation):
# Reference: src/adcp_mcp/conversion/ip_crosswalk.py (maraketa)
dist = disk_to_cells(lat, lng, radius_m, resolution=8,
confidence=0.67, convention="confidence",
kernel="uniform")
decision = match(dist, declared_cells, rule="threshold", threshold=0.6)
# decision.in_set_mass, decision.expected_overreach — reported either way
The uniform kernel needs zero vendor cooperation: center + radius +
convention is enough, and it is conservative by construction because the
tail is never assigned to any cell. A vendor-published empirical
distribution per block is strictly better and slots into the same
{cell, p} shape — that is a supplied declaration, not a free-rail
computation (see the extension points page).
Parameters
Resolution (capped to accuracy), confidence convention (mandatory), kernel
(uniform default, gaussian optional), match rule and threshold where
applicable, and the database vintage carried on every output row.
Outputs
Weighted {cell, p} rows at the stated resolution, the explicit
residual_mass, the center cell (for auditing the center-rule counterfactual),
and the crosswalk provenance: source, vintage, valid_from/valid_to,
confidence convention.
Quality metrics
The polygon-era coverage_ratio has no meaning here; the native metric is
expected overreach — 1 − mean in-set mass across matched lookups —
computable before delivery from the declared distribution and verifiable
after against any ground-truth sample. Track alongside it: the fraction of
lookups whose accuracy radius exceeds the cell scale (same proxy as for
device pings), the modal-cell share (how much mass the top cell carries — a
distribution whose top cell carries ~1.0 is a point pretending to be a
disk), and the forecast-versus-realized overreach gap, which is a running
calibration audit of the supplying vendor.
Edge cases
Centroid pile-up is the signature failure: assigning each lookup to its
most-likely cell stacks every ambiguous impression onto vendor fallback
centroids — city centers, ZIP centroids — manufacturing density spikes
uncorrelated with population or POI mass; apportion fractionally instead,
and never report modal-cell rows as measured. Convention mismatch silently
halves or doubles containment when ~67% and CEP50 radii are pooled
unconverted. Vintage arm-flips are the experimentation-critical case: pin
the vintage per assignment period, re-pin only at period boundaries, and
count, report, and exclude flipped blocks. And the matching-semantics rule
from the advertising page still binds — IP-derived location is never
physical_presence, whatever the radius says; a tight radius is a precision
claim about a signal that fails on the semantic axis first.
Assumptions and limitations
This conversion assumes the vendor's declared radius is honest at the declared confidence. That assumption is checkable only through a calibration attestation — measured containment of the declared disk against a GPS ground-truth sample, per country or region — and a declared radius without one is a claim, not a bound. The uniform and Gaussian kernels are models; both are isotropic, and real IP error follows ISP infrastructure, which is anisotropic. The framework's protection against both gaps is the same: expected overreach is declared up front and audited against realized delivery, so a miscalibrated vendor is detected after one flight rather than trusted indefinitely.
