All sections

Google-Style Point-Radius Execution

Executing an H3 cell set on a platform that only accepts many independent point-and-radius targets, one circle per cell

approximateplatform-dependentstableh37 min read
Source geometry
h3_cell_set
Destination geometry
point_radius

Purpose

Google Ads, most DSPs, and most mobile geofencing SDKs share a common execution primitive: a location target is a circle — a point plus a radius, subject to a platform-specific minimum radius, a rounding increment, and a cap on how many targets a single campaign or ad group may carry. Executing an H3 cell set on such a platform is not one conversion but a repeated application of a circle choice — one circle per cell — subject to those platform constraints. This page is the execution-layer counterpart to the single-cell circle pages; it covers what changes when "one circle" becomes "many circles under a budget."

Source geometry and destination geometry

Source is an h3_cell_set at a stated resolution. Destination is a list of point_radius targets, one per cell (in the naive case) or fewer, after optimization (see below) — each entry a center, a radius, and whatever platform-native target id the upload API returns.

Exactness class

Approximate, and for two independent reasons layered on top of each other: first, whichever single-cell circle is chosen (inner, outer, or equal-area) already carries that construction's own gap or overlap behavior; second, the platform's own radius rounding and minimum-radius floor perturb the chosen radius again after it is computed. Never describe this as "targeting the H3 cells" without naming both the circle mode and the platform's rounding behavior.

Containment rule and boundary behavior

Inner (inscribed) per cell
One circle per cell, no cross-cell overlap, systematic gaps at every cell's corners. Use when double-counted reach or double-billed impressions across adjacent cells is the primary risk to avoid.
Outer (circumscribed) per cell
One circle per cell, guaranteed per-cell coverage, systematic overlap with every neighboring cell's circle. Use when under-delivery to any part of a targeted cell is the primary risk to avoid.
Equal-area per cell
One circle per cell, area-matched but neither contained by nor containing its cell; gaps and overlaps both present. Use only for reach/budget planning math upstream of the actual buy, never as the executed target list itself.
Circumscribed circles overlap at seams → duplicate eligibility
Circumscribed circles overlap at seams → duplicate eligibility
Inscribed circles leave corner gaps → no overlap
Inscribed circles leave corner gaps → no overlap

Whichever mode is chosen, the platform performs its own deduplication across overlapping circles at the audience level — a device inside two overlapping outer circles is generally billed and reported once by the platform's own frequency logic, not twice — but that dedup logic is platform-internal and not something this conversion can verify from the geometry alone. Treat duplicate_eligibility_area (duplicateEligibilityAreaM2 on the actual circle set) as the upper bound on the platform's exposure to double-counting, not as a guarantee of what the platform actually reports.

Resolution behavior

Coarser resolutions mean fewer, larger circles — cheaper against a target-count cap, but each circle's absolute overreach or underreach grows with cell size even though the relative ratio (per the single-cell circle pages) stays roughly constant for regular hexagons. Finer resolutions mean more, smaller, tighter circles that are more likely to collectively exceed a platform's target-count cap before they exceed its accuracy needs — the resolution choice here is frequently constrained by the target-count limit first and the desired precision second.

Units and CRS

Centers are EPSG:4326 decimal degrees; radii are meters unless the platform's upload API specifies otherwise (some ad platforms accept radius in miles or kilometers and round differently in each unit — verify the platform's documented unit before submitting, since a silent unit mismatch is functionally a 1.6× or 0.62× scale error on every target).

Algorithm

function executeAsPointRadius(cells, mode, platformLimits):
    circles = []
    for cell in cells:
        c = computeCircle(cell, mode)          # inscribed | circumscribed | equalArea
        r = clamp(c.radiusMeters, platformLimits.minRadius, platformLimits.maxRadius)
        r = roundToIncrement(r, platformLimits.radiusIncrement)
        circles.append({ center: c.center, radiusMeters: r })
    if len(circles) > platformLimits.maxTargets:
        circles = optimizeCircleCover(cells, platformLimits)  # advanced, see below
    return circles
import { circumscribedCircle, inscribedCircle, equalAreaCircle } from "@/lib/h3/circles";

function toPlatformTargets(
  cells: string[],
  mode: "inscribed" | "circumscribed" | "equal_area",
  minRadiusM: number,
  radiusIncrementM: number,
) {
  const build = mode === "inscribed" ? inscribedCircle
    : mode === "circumscribed" ? circumscribedCircle
    : equalAreaCircle;
  return cells.map((cell) => {
    const c = build(cell);
    const rounded = Math.max(
      minRadiusM,
      Math.ceil(c.radiusMeters / radiusIncrementM) * radiusIncrementM,
    );
    return { lat: c.center[0], lng: c.center[1], radiusMeters: rounded };
  });
}

The same conversion with the Python bindings (h3-py v4):

import math
import h3

# Reuses the same densify-and-measure routines from the inscribed/
# circumscribed circle pages; only the aggregation (min vs max vs area)
# and the platform clamp/round step differ per mode.

def circle_for_mode(cell: str, mode: str):
    center = h3.cell_to_latlng(cell)
    if mode == "equal_area":
        area_m2 = h3.cell_area(cell, unit="m^2")
        return center, math.sqrt(area_m2 / math.pi)
    boundary = h3.cell_to_boundary(cell)
    n = len(boundary)
    dists = []
    for i in range(n):
        a, b = boundary[i], boundary[(i + 1) % n]
        for sample in densify_edge_geodesic(a, b):  # see h3-to-inscribed-circle
            dists.append(h3.great_circle_distance(center, sample, unit="m"))
    if mode == "inscribed":
        return center, min(dists) * (1 - 1e-4)
    if mode == "circumscribed":
        return center, max(dists) * (1 + 1e-4)
    raise ValueError(f"unknown mode: {mode}")

def to_platform_targets(cells, mode, min_radius_m, radius_increment_m):
    targets = []
    for cell in cells:
        (lat, lng), radius_m = circle_for_mode(cell, mode)
        rounded = max(
            min_radius_m,
            math.ceil(radius_m / radius_increment_m) * radius_increment_m,
        )
        targets.append({"lat": lat, "lng": lng, "radiusMeters": rounded})
    return targets

h3-py provides no per-cell circle helper for any of the three modes and no minimum-radius/rounding logic — both are reproduced here: the min/max great-circle-distance aggregation over a densified boundary for inscribed/circumscribed (identical algorithm to the dedicated circle pages), the area-only formula for equal-area, and a plain max/ceil clamp-and-round for the platform floor and increment. The tested reference implementation is the TypeScript in lib/.

Parameters

Circle mode (inner/outer/equal-area), the platform's minimum radius, maximum radius, radius rounding increment, and maximum target count per campaign/ad-group — all platform-specific and none safe to assume from another platform's documented limits.

Outputs

A list of { center, radiusMeters, platformTargetId } entries, tagged with the circle mode used and the resolution the source cell set was normalized to, so a later audit can reconstruct why a given radius does not exactly match circumscribedCircle(cell).radiusMeters (rounding, or a min-radius floor, will have moved it).

Quality metrics

overreach_ratio and underreach_ratio from the underlying circle mode still apply per cell, plus duplicate_eligibility_area across the full target set once rounding has been applied — rounding up to a minimum radius or to the next increment always increases overreach relative to the raw circle, never decreases it, since the rounding direction favors meeting the platform's floor.

Edge cases

A minimum-radius floor turns a small, fine-resolution cell's already-small inscribed circle into a circle much larger than the cell itself once the floor is applied — at that point the "inner" circle mode no longer has its containment guarantee, because the platform, not this conversion, has overridden the radius. Radius rounding to a platform's fixed increment (for example, whole kilometers or quarter-miles) means the delivered radius is never exactly the computed one; round up when the intent was outer/coverage-guaranteeing and round down when the intent was inner/containment-guaranteeing, and document which direction was used, since rounding in the wrong direction silently flips a construction's guarantee.

Advanced: optimized circle cover

When the naive one-circle-per-cell list exceeds a platform's target-count cap, an experimental alternative is to solve a small circle-cover problem instead: cluster nearby cell centers, replace several small circles with fewer, larger ones sized to bound total overreach, and stop once the target count is under the cap. This is the platform_limit_optimized conversion profile — containmentMode: intersect, circleMode: circumscribed, resolution policy "compact and coarsen until under the cap and above the minimum radius," with the profile's own stated guarantee limited to "respects maxTargets and minRadius for the named platform" and its stated tradeoff explicit: "coarsening enlarges the effective footprint." Treat this path as experimental and always report overreach_ratio and coverage_ratio against the original cell set, not just against the coarsened one, since the coarsening step itself is a second, compounding approximation on top of the circle choice.

Assumptions and limitations

This conversion assumes the platform's declared minimum radius, maximum radius, rounding increment, and target-count cap are current — these change per platform and per ad product without notice, and a value cached from a prior integration can silently violate the platform's actual current limits. It also assumes the platform's own cross-target deduplication is at least as conservative as the geometry suggests; verify this against platform documentation rather than assuming it, since duplicate_eligibility_area computed here is a geometric upper bound, not a report of what the platform will actually bill or attribute.

Edge cases affecting this page
  • - A platform floor (e.g. 1 km) makes sub-floor cells un-executable as circles.
  • - Platforms round radii to increments, changing coverage/overlap.