Polyfill: turn a polygon into the set of H3 cells that cover it, so a spatial question bec
Polyfill: turn a polygon into the set of H3 cells that cover it, so a spatial question becomes a set-membership question you can answer with a join. Send "geojson" as a Polygon, a MultiPolygon, or a Feature or FeatureCollection wrapping them, in EPSG:4326, plus exactly one of "resolution" or "resolutions". Choose the containment rule with "containment": "center" returns cells whose centre is inside the polygon, which is H3's default and under-covers at the edges; "overlapping" returns every cell the polygon touches, so the set fully covers it; "full" returns only cells entirely inside. Holes are respected, a MultiPolygon is treated as one polygon with its parts unioned and deduplicated, and feature ids are echoed so rows join back to your records. This route covers polygons and nothing else: a body carrying "points" is refused rather than served, and because "geojson" is required here that refusal happens before a price is quoted and costs you nothing. To index individual positions to their containing cell, POST /v1/h3/index instead, which is cheaper because it is a much smaller job. What you are paying for here, measured on this runtime: cell count grows about sevenfold per resolution step, so the cost has to be estimated before any cell is generated, which puts a floor of about 1.5 ms on even a city-block polyfill against about 0.003 ms for a one-point index call, and a polyfill at the 100000-cell cap is about 200 ms and 1.8 MB. Anything estimated above that cap is refused with the finest resolution that would have fitted, before a cell is generated; set "estimateOnly" to size a job for the price of the call and nothing else. Input is EPSG:4326, longitude then latitude, and non-areal geometry is refused by name. Limits: 500 polygons, 10000 polygon positions, 4 resolutions per call, 100000 cells per call, and a 1 MiB request body.
Provider
api.geoprimitives.dev · discovered, not yet claimed by its owner
Payment (x402 accepts[])
[
{
"scheme": "exact",
"network": "eip155:8453",
"payTo": "0x76A51b5Dd3729950B3b60c17f19252d221968FCe",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000",
"maxTimeoutSeconds": 300
}
]Output schema
{
"bazaar": {
"info": {
"input": {
"body": {
"geojson": {
"coordinates": [
[
[
-122.43,
37.77
],
[
-122.4,
37.77
],
[
-122.4,
37.79
],
[
-122.43,
37.79
],
[
-122.43,
37.77
]
]
],
"type": "Polygon"
},
"resolution": 8
},
"bodyType": "json",
"method": "POST",
"type": "http"
},
"output": {
"example": {
"cellCount": 9,
"containment": "center",
"mode": "polyfill",
"notes": [],
"polygonCount": 1,
"resolutions": [
8
],
"results": [
{
"cellCount": 9,
"cells": [
"8828308283fffff",
"882830828bfffff",
"88283082b9fffff",
"8828308281fffff",
"8828308287fffff",
"88283082e3fffff",
"88283082abfffff",
"88283082bdfffff",
"8828308285fffff"
],
"estimatedCellCount": 15,
"id": null,
"polygon": 0,
"resolution": 8
}
]
},
"type": "json"
}
},
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"input": {
"additionalProperties": false,
"properties": {
"body": {
"additionalProperties": false,
"oneOf": [
{
"required": [
"resolution"
]
},
{
"required": [
"resolutions"
]
}
],
"properties": {
"containment": {
"description": "\"center\" returns cells whose centre is inside the polygon, the H3 default, which under-covers at the edges. \"overlapping\" returns every cell the polygon touches, so the set fully covers it. \"full\" returns only cells entirely inside it. Defaults to \"center\".",
"enum": [
"center",
"overlapping",
"full"
],
"type": "string"
},
"estimateOnly": {
"description": "Return the estimated cell count per polygon and resolution without generating any cells. The estimate is an upper bound, never lower than the true count. Defaults to false.",
"type": "boolean"
},
"geojson": {
"description": "Polygons to cover: a Polygon, a MultiPolygon, or a Feature or FeatureCollection wrapping them, in EPSG:4326. At most 500 polygons and 10000 positions in total. Non-areal geometry is rejected. Required: this route takes nothing else. To index individual positions, POST /v1/h3/index.",
"type": "object"
},
"resolution": {
"description": "H3 resolution, 0 (about 4.4 million square kilometres per cell) to 15 (about 0.9 square metres). Send this or \"resolutions\", not both. One step finer is about seven times as many cells.",
"maximum": 15,
"minimum": 0,
"type": "integer"
},
"resolutions": {
"description": "Several H3 resolutions at once, 0 to 15, distinct. At most 4, and all of them are counted against the one 100000-cell cap.",
"items": {
"maximum": 15,
"minimum": 0,
"type": "integer"
},
"maxItems": 4,
"type": "array"
}
},
"required": [
"geojson"
],
"type": "object"
},
"bodyType": {
"enum": [
"json",
"form-data",
"text"
],
"type": "string"
},
"method": {
"enum": [
"POST"
],
"type": "string"
},
"type": {
"const": "http",
"type": "string"
}
},
"required": [
"type",
"method",
"bodyType",
"body"
],
"type": "object"
},
"output": {
"properties": {
"example": {
"type": "object"
},
"type": {
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
},
"required": [
"input"
],
"type": "object"
}
}
}Use it
curl
curl "https://api.geoprimitives.dev/v1/h3/polyfill" # -> 402 Payment Required, accepts[] lists how to pay # retry with a PAYMENT-SIGNATURE (or PAYMENT header) once paid
JavaScript
const res = await fetch("https://api.geoprimitives.dev/v1/h3/polyfill");
if (res.status === 402) {
const { accepts } = await res.json();
// pay one of accepts[] via an x402 client, then retry with the payment header
}Python
import httpx
res = httpx.get("https://api.geoprimitives.dev/v1/h3/polyfill")
if res.status_code == 402:
accepts = res.json()["accepts"]
# pay one of accepts[] via an x402 client, then retry with the payment headerMachine-readable
Everything on this page is also available as clean JSON at /resources/6754.json, and this resource appears in /discovery/resources and /discovery/search.