Skip to content

RFC: geo_cells / geo_cells_exclude targeting — hierarchical cell-grid systems as a declared geographic unit #5969

Description

@EvgenyAndroid

Summary

Add geo_cells / geo_cells_exclude to the targeting overlay, a new enums/cell-system.json enum (h3, s2, geohash, custom), and an explicit geo_cells property on execution.targeting in get_adcp_capabilities. This is a standalone, non-breaking additive change against current stable.

The proposal adds a unit, not a mechanism. Cell entries inherit the hard-restriction enforcement semantics already established for geo overlays (#5756) — capability pre-screening before create_media_buy, seller-obligated honoring, INVALID_REQUEST on unsupported fields. No new enforcement path.

Revised 2026-07-26. This body folds in the four required fixes and the Option C capability shape accepted in-thread, the X-B scope resolution and its Fix 3 sentence, the privacy-considerations note, and live links to the two companions (#6013, #6014). See the revision note at the end for a line-by-line changelog.


Problem

The targeting overlay's geographic vocabulary is keyed to administrative units (geo_countries, geo_regions, geo_metros, geo_postal_areas) plus point-based geo_proximity. Administrative units have three structural limits for measurement-grade targeting:

  1. They don't interoperate across borders. metro-system.json enumerates nielsen_dma, uk_itl1, uk_itl2, eurostat_nuts2 — four incompatible systems for one concept. A multi-market geo experiment cannot use a consistent randomization unit.
  2. They come in one resolution each. DMAs (~210 in the US) are too coarse for high-powered geo-split designs; postal areas vary wildly in size and are unstable across census vintages. There is no clean ladder between them.
  3. They redraw. ZIP boundaries change with each decennial vintage. A unit that moves cannot anchor longitudinal measurement.

Why geo_proximity.geometry doesn't already cover this. A buyer can convert cells to polygons and send them via geo_proximity.geometry today, so the delivery footprint is achievable with the current spec. But geo_proximity entries are trade areas from arbitrary points, not units of analysis: they don't tile, don't roll up hierarchically, don't compose across campaigns, and carry no stable identifier a delivery report or experiment can reference. Polygons solve delivery; they cannot solve measurement.

Hierarchical cell grids provide the missing properties by construction: globally uniform, multi-resolution with clean parent/child rollup, and fixed for all time. The unit is purely additive — sellers keep their native geo encodings and convert in and out via open-source warehouse-native tooling.


Proposed changes

1. New enum: static/schemas/source/enums/cell-system.json

Structurally parallel to metro-system.json, including the custom token.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "/schemas/enums/cell-system.json",
  "title": "Cell Grid System",
  "description": "Hierarchical cell-grid systems for geographic targeting. All listed systems are open specifications.",
  "type": "string",
  "enum": ["h3", "s2", "geohash", "custom"]
}

All three named systems are open specifications (h3: Apache-2.0, s2: Apache-2.0, geohash: public domain). Three systems rather than h3-only: s2 is the native cell system for GCP/Google Maps infrastructure; geohash is embedded in Elasticsearch, PostGIS, Redis GEO, and most document stores with a geo index. An h3-only enum forces sellers on those stacks through a coordinate transform where none is needed. Three systems also clarifies custom: it is for genuinely proprietary systems, not a fallback for well-known ones.

Cross-party identification of custom systems (custom_system_id) is specified in the conversion companion (#6013) rather than here.

2. static/schemas/source/core/targeting.json: add geo_cells and geo_cells_exclude

Mirrors geo_metros item shape (no additionalProperties constraint, matching live schema precedent), with resolution added as a third required property.

"geo_cells": {
  "type": "array",
  "description": "Restrict delivery to specific grid cells. Each entry declares the cell system, the resolution, and cell indexes at that resolution. Seller must declare supported systems and resolutions in get_adcp_capabilities.",
  "items": {
    "type": "object",
    "properties": {
      "system": {
        "$ref": "/schemas/enums/cell-system.json"
      },
      "resolution": {
        "type": "integer",
        "minimum": 0,
        "description": "Cell resolution within the declared system (e.g., 0–15 for h3, 0–30 for s2, 1–12 for geohash). Valid range is system-specific and validated by the seller, not the schema. All values in this entry MUST be valid cell indexes at this resolution."
      },
      "values": {
        "type": "array",
        "description": "Cell indexes within the system (e.g., ['882a100d25fffff'] for an h3 r8 cell). H3 values MUST be in canonical lowercase hex form.",
        "items": { "type": "string" },
        "minItems": 1
      }
    },
    "required": ["system", "resolution", "values"]
  },
  "minItems": 1
}

geo_cells_exclude takes the identical shape, consistent with the existing *_exclude family.

Normative validation requirements (seller-enforced, not schema-encoded). Per-system resolution ranges and cell index format are not encoded in the JSON schema — mirroring how postal system codes are not regex-validated. However, sellers MUST:

  • Resolution agreement (Fix 1). The resolution declared on an entry MUST agree with the resolution encoded in every entry in values[]. All three named systems encode resolution intrinsically (h3 in the index, s2 in the token, geohash in string length), so cross-field drift is detectable even though JSON Schema cannot express it. Sellers MUST reject disagreeing entries with INVALID_REQUEST and the offending field path.
  • Reject entries whose resolution is outside the valid range for the declared system with INVALID_REQUEST and the offending field path.
  • Reject entries whose values contain strings that are not valid cell indexes at the declared resolution, with INVALID_REQUEST and the offending field path.
  • Reject H3 values not in canonical lowercase hex form with INVALID_REQUEST. Silent case-insensitive acceptance would create two valid representations of the same cell, breaking string-equality key lookups.

Mixed-resolution entries within one system are a buyer-side bug; deterministic rejection is more useful than silent coercion.

3. Delivery obligation and its boundary (Fix 3)

A geo_cells overlay is a delivery-routing constraint. The seller MUST NOT deliver impressions outside the declared cell set, using whatever internal geo representation it chooses (native cell support, admin-unit crosswalk, or polygon conversion via geo_proximity.geometry semantics).

The restriction binds the output footprint, not the seller's internal representation. A conversion whose executed footprint extends beyond the declared cell set is non-conformant, regardless of the internal method that produced it. Conservative conversions (exact cell-boundary polygons, inscribed circles, fully-contained admin crosswalks) satisfy this; overshoot conversions (circumscribed circles, intersection-mode crosswalks) do not.

Cell-grain attribution is a separate capability. It is declared in reporting_capabilities and requested via reporting_dimensions (#6014). Supporting the overlay does not imply cell-grain reporting, and vice versa.

This is the complete normative surface required for correctness, including experiment safety: overshoot is what contaminates a holdout, and overshoot is already non-conformant here. Footprint identity between two conformant sellers — one inscribing circles, one polyfilling whole cells, both staying inside the declared set — is an optimization rather than a safety property, and is specified in #6013 rather than gating this RFC (see Resolved design questions).

4. Capability declaration in get_adcp_capabilities response schema

Add geo_cells as an explicit named property on media_buy.execution.targeting. The live execution.targeting object does not use additionalProperties: false at the top level, so this is an accepted additive sibling of geo_metros.

Resolution is a precision tier, not a vocabulary choice, so the right precedent is keyword_targets.supported_match_types rather than geo_metros booleans — a boolean-to-object migration later would be a breaking change. Per-system field names follow each system's native terminology.

"geo_cells": {
  "type": "object",
  "additionalProperties": true,
  "description": "Cell-based targeting. Absent system key = system not supported. Absent resolution field within a system = all resolutions supported; present and non-empty = constrained to listed values.",
  "properties": {
    "h3": {
      "additionalProperties": true,
      "properties": {
        "supported_resolutions": {
          "type": "array", "minItems": 1, "uniqueItems": true,
          "items": { "type": "integer", "minimum": 0, "maximum": 15 }
        }
      }
    },
    "s2": {
      "additionalProperties": true,
      "properties": {
        "supported_levels": {
          "type": "array", "minItems": 1, "uniqueItems": true,
          "items": { "type": "integer", "minimum": 0, "maximum": 30 }
        }
      }
    },
    "geohash": {
      "additionalProperties": true,
      "properties": {
        "supported_precisions": {
          "type": "array", "minItems": 1, "uniqueItems": true,
          "items": { "type": "integer", "minimum": 1, "maximum": 12 }
        }
      }
    },
    "custom": { "additionalProperties": true }
  }
}

Absence semantics (Fix 4). An empty array is never valid (minItems: 1); absence is the single way to express "all." A seller that does not declare a system is disqualified from receiving that system's cell targeting before create_media_buy — no new pre-screening semantics. The custom key exists so sellers supporting proprietary grids have a conformant declaration path (Fix 2); its identification field is specified in #6013.

5. Non-normative documentation

  • docs/media-buy/advanced-topics/targeting.mdx: extend the existing RCT-holdout worked example with a cell-based variant (treatment/holdout assignment at declared resolution).
  • New guidance page (docs/media-buy/advanced-topics/geo-units.mdx): resolution selection (statistical power vs. footprint), weighted crosswalks between cells and metro/postal/regions, interim delivery path via geo_proximity.geometry polygon conversion for sellers without native cell support, and the US statutory "precise geolocation" boundary (the 1,750 ft radius definition falls between H3 r7 and r8, so resolution choice doubles as a compliance declaration).

Privacy considerations. A seller declaring support for a given resolution is making a precision claim and a privacy claim at once. Resolution alone is not a privacy control — risk is driven by the population inside a cell, not the cell's area. Sellers supporting fine resolutions in sparse geographies need a stated aggregation or suppression strategy (both a resolution floor and an audience floor, declared together) for cells that fall below it, and should not silently roll a sparse cell into its parent — see #6014 §6 for the reporting-side version of this same resolution. Because cell IDs discretize location rather than exposing an exact point, clean-room mediation is not inherently required at the resolutions this RFC's use cases target; that property is a function of the declared resolution and cell population, not of the cell system.


Resolved design questions

Question Resolution
Three-system enum vs. h3-only? Three systems (h3, s2, geohash). Native-system avoidance of forced coordinate transforms on GCP/Elasticsearch stacks; custom purpose is clearer with three named alternatives.
Per-system resolution bounds: if/then schema vs. SDK invariant? Seller-enforced, normatively specified but not schema-encoded. Mirrors postal-code precedent; draft-07 if/then across three systems adds maintenance burden without meaningful validator benefit given seller-side INVALID_REQUEST enforcement.
H3 index form: canonical lowercase vs. case-insensitive? Canonical lowercase required, seller MUST reject otherwise. Two representations of one cell break string-equality key lookups.
3.1 scoping vs. standalone additive? Standalone additive. Non-breaking; bundling into 3.1 couples timing to a heavier milestone unnecessarily.
geo_cells vs. geo_postal_areas buyer guidance? Determined by measurement stack, not protocol preference. If downstream analysis is postal-keyed, use postal targeting and crosswalk at analysis time; if outcome data is already cell-keyed, use cell targeting directly.
Capability shape: booleans vs. object? Object, per-system field names, minItems: 1 / uniqueItems: true, additionalProperties: true throughout. Resolution is a precision tier, not vocabulary membership; a boolean→object migration later would be breaking.
Empty supported_resolutions = all resolutions? No — superseded. Empty arrays are invalid (minItems: 1); absence of the field means all resolutions. Empty-means-all inverts every other array in the schema.
X-A (normative conversion rules pre-merge) vs. X-B (hard restriction sufficient)? X-B, plus the output-footprint sentence in §3. Overshoot is already non-conformant under the delivery restriction, so contamination — the experiment-fatal case — is excluded without containment-mode logic. Footprint identity between conformant sellers is an optimization and is specified in #6013.

Companions (linked, not bundled)


Affected spec files

  • static/schemas/source/enums/cell-system.jsonnew
  • static/schemas/source/core/targeting.json — add geo_cells, geo_cells_exclude
  • static/schemas/source/protocol/get-adcp-capabilities-response.json — add explicit geo_cells property on media_buy.execution.targeting
  • docs/media-buy/advanced-topics/targeting.mdx — cell-based RCT worked example
  • docs/media-buy/advanced-topics/geo-units.mdxnew: crosswalks, resolution selection, interim geometry path, compliance and privacy notes

Related


Revision note (2026-07-26)

Changes from the original filing, all traceable to in-thread acceptance:

  1. §2 — added the Fix 1 resolution-agreement MUST as an explicit normative bullet.
  2. §3 — new section. Fix 3 delivery-routing vs. attribution prose, plus the accepted output-footprint sentence resolving X-A/X-B.
  3. §4 — capability block replaced with the Option C shape: additionalProperties: true throughout, per-system field names (supported_resolutions / supported_levels / supported_precisions), minItems: 1 / uniqueItems: true, and a custom key (Fix 2 — the previous block made custom undeclarable).
  4. §5 — added the privacy-considerations note (joint resolution + audience floor; no silent parent rollup; clean-room mediation not inherently required at target resolutions).
  5. Resolved design questions — the empty-array row is marked superseded (Fix 4); rows added for capability shape and the X-A/X-B outcome.
  6. Companions — placeholder references replaced with RFC: Cell Interoperability and Execution Conversion — companion to #5969 #6013 and RFC: Cell-grain delivery reporting — companion to #5969 #6014.

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.media-buyIssue concerns the media-buy protocol domainneeds-wg-reviewBlocked on a working-group decision — surface in WG meeting agendasrfcProtocol change — auto-adds to roadmap boardschemaJSON Schema source-of-truth: definitions, codegen artifacts, validation, hygienespec / protocol

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions