Coverage Footprints
Where the coverage overlays' footprints come from, and how the 3D and LiDAR ones are built
The Coverage Overlays draw where each source has data. This page is how those footprints are made.
Where the footprints come from
| Group | Geometry | Source |
|---|---|---|
| Mapterhorn | Vector tiles, per national source | Mapterhorn coverage tiles (single-archive-tiles.mapterhorn.com/coverage/{z}/{x}/{y}.mvt) + attribution.json |
| Terrain / basemap library | Bounds rectangles | bounds of each entry in lib/custom-sources.json |
| OSM Editor Layer Index | Coverage polygons | @osm-editor-kit/maplibre-editor-layer-index (bundled index, bumped weekly) |
| Your sources | Bounds rectangles | the bounds you declared in the source dialog |
| Bing Maps 3D | Merged tile rectangles, ~2.4 km | Bing's own 3D Tiles subtree availability (td1 manifest → st… subt bitstreams), crawled by docs/scripts/build-bing-3d-coverage.mjs into public/coverage/bing-3d.geojson — no API key, nothing published lists it |
| Google 3D | Decoded polygons, dissolved, ~500 m | Google’s own coverage layer, fetched from maps.googleapis.com/maps/vt at z8 with a layer id minted from an API key, decoded (XOR 0x9b + protobuf + triangle mesh) by docs/scripts/google3d-coverage.ts, then unioned and simplified by docs/scripts/dissolve-google-3d-coverage.mjs into public/coverage/google-3d.geojson |
| FLAI open LiDAR | Traced octree cells, per dataset | Each survey’s COPC octree hierarchy — the overview file’s occupied nodes, read over range requests without downloading any points — by docs/scripts/build-flai-coverage.mjs into public/coverage/flai-open-lidar.geojson. FLAI’s API carries no geometry at all |
| Esri Integrated Mesh | One rectangle per scene layer | ArcGIS Online’s public search API, which returns each item’s extent, by docs/scripts/build-esri-3d-coverage.mjs into public/coverage/esri-3d.geojson — no key, no crawl. Matched on typeKeywords:"IntegratedMesh", which ArcGIS sets itself when the layer is published; the free-text tag "integrated mesh" finds 48 layers where the type keyword finds 10 900. Queried a year at a time, since search caps any one query at 10 000 items. Deduped by service URL, since the same hosted mesh is routinely registered as several items |
| OpenTopography rasters / point clouds | Dataset footprints (outlines where the catalog has them, boxes otherwise), ~100 m | OpenTopography's public catalog API (/API/otCatalog, hosted datasets, CORS open), baked by scripts/build-opentopo-coverage.mjs into public/coverage/opentopo-raster.geojson and opentopo-pointcloud.geojson; the popup opens the dataset page with its DOI and downloads. Federated USGS 3DEP and NOAA collections are left out |
How the 3D and LiDAR footprints are built

The four 3D and LiDAR overlays on one switch, over Europe - Bing (indigo), Google (rose), Esri Integrated Mesh (violet), FLAI (teal)
None of these is built by the deploy workflow. Each is a script under docs/scripts/, run by hand, whose output is committed under public/coverage/; the GitHub Action only builds the app and this docs site. Times are from one machine on 2026-09-24.
Google photorealistic 3D
Google publishes no machine-readable coverage. The footprints are not rasterised, not screen-scraped from the Maps JavaScript API, and not derived from the 3D Tiles API — that tree refines to 2 m over rural Nepal exactly as over central Paris, so its depth says nothing about photogrammetry (measured by probe-google-3d-detail.mjs). It is Google's own coverage layer, the one drawn on the Photorealistic 3D Tiles coverage page, which turns out to be an ordinary Maps vector layer: tiles from maps.googleapis.com/maps/vt with a layer id (ml:xs:c:…) that mapConfigs:batchGet mints from a plain API key. A tile body is XOR 0x9b, then a protobuf of a pre‑triangulated mesh in tile‑local integer coordinates on an 8 192 grid.
The bug that hid behind every other fix, for the record: the decoder read each vertex delta with protobuf's zigzag rule (v/2, −(v+1)/2), and this format is not zigzag — it wants v / −(v+1). Every polygon came out deflated 2× about its first vertex, so each zoom covered exactly (½)² ≈ 25 % of central Paris, different zooms' pieces landed in different places, unions grew with every zoom added and never converged, and London and San Francisco tested "outside". That was misread, in turn, as generalisation, as a per‑tile budget and as zoom sharding. Jonathan spotted the 2× from the shape of the polygons in kepler.gl. The rule was settled by cross‑zoom agreement: with it, the central‑Paris z10 tile and its four z11 children render the same polygons (Jaccard 1.00, 96 % of the tile covered); with zigzag they were near‑disjoint (0.32, 23 %). Decoded correctly the layer is an ordinary generalised one — every zoom from z5 to z12 gives ~2.05 Mkm² worldwide and 91–96 % of that tile — so one zoom is all it takes, and the union‑of‑zooms machinery is gone.
Three stages, in this order:
- Fetch —
docs/scripts/google3d-coverage.ts fetch, one hierarchical crawl from z5 to z8: an empty tile is a fixed 36‑byte stub, so only non‑empty children are descended. 4 120 requests, 32 MB, about 10 s, into.cache/google3d/. - Decode —
google3d-coverage.ts decode --zoom 8: XOR, protobuf, triangles → rings by cancelling shared edges. Two things earlier decoders got wrong and that silently delete cities: zero‑area collinear triangles are part of the mesh and must be counted, and vertices must keep their ~0.7 % overshoot rather than be clamped to the tile box. Coordinates go from the 8 192 grid to lng/lat, so the only quantisation is the tile's own, 19 m at z8. Output.cache/google3d-z8.geojson, 17 937 polygons, about 5 s — the file to load in kepler.gl for the raw layer. The decoder was audited stage by stage on the central‑Paris tile: triangles, rings and polygons all agree to 0.3 %. - Dissolve —
docs/scripts/dissolve-google-3d-coverage.mjs: snap every ring to 3 decimals (~110 m) and clean it, drop anything under 1 000 m²; bucket by 5°×5°; union each bucket in chunks of 400, thin each partial (125 m), tree‑merge the partials pairwise until nothing merges; then per merged polygon drop holes under 1 km², simplify at ~500 m, truncate to 3 decimals, re‑node by unioning the polygon with itself, drop small holes again, drop outer rings under 1 km². 17 937 polygons → 1 601, 0.99 MB, 49 s. Outputpublic/coverage/google-3d.geojson. The re-noding and the hole dropping clean up what generalising a union of tile‑clipped triangles leaves behind: sub‑kilometre holes where triangles met imperfectly, and self‑intersections made by the simplification itself.
Checked: central Paris 97.1 % of its tile; the wider Paris window is a single polygon of 2 894 km² with no overlaps; 20/20 on a city checklist. z9 and z10 give the same answer to within 0.2 % at the same file size — z8 is the level whose blocks read like Google Earth’s own coverage view, and it is the cheapest to crawl.
docs/scripts/google3d-coverage.ts
docs/scripts/dissolve-google-3d-coverage.mjs
.cache/google3d-z8.geojson (raw decode, 17 937 polygons)
public/coverage/google-3d.geojson (shipped, 1.0 MB)pnpm google-3d-fetch (needs GOOGLE_KEY) then pnpm google-3d-dissolve reproduces it in about a minute. The same endpoint and tile format also serve the layer Google Earth draws for “3D buildings where available” (ml:xsr:c:…, the id from any captured Earth bpb= request): fetch --layer-id … crawls it with no API key at all and yields blockier per‑region blobs in tiles about 4× smaller — the “which cities are covered” view rather than the parcel‑level one. Superseded but kept: build-google-3d-coverage.mjs classifies tiles by response size and needs no key (~39 km cells — the fallback if the layer id minting ever stops), build-google-3d-coverage.py is a shapely port of the decoder (delta rule fixed in step), and decode-google-3d-coverage.mjs is the earlier hand‑rolled decoder.
Bing Maps 3D
Bing serves its tf=3dv4 photogrammetry as a standard OGC 3D Tiles 1.1 implicit tileset rooted at the td1 manifest: four Web Mercator faces, each a quadtree of subt subtree binaries whose contentAvailability bitstream says exactly which tiles carry a mesh. build-bing-3d-coverage.mjs reads the 4 root subtrees, then every level‑7 subtree they mark (469), emits the content tiles at level 13 (161 276) and merges adjacent ones into rectangles. No key, 6 s. Exact by construction — it is the service's own availability data.
FLAI open LiDAR
FLAI's public API lists 114 datasets and carries no geometry. Each dataset has an overview COPC on a public bucket, and a COPC is an octree with a published index, so build-flai-coverage.mjs range‑reads the 375‑byte LAS header, the copc info VLR and the hierarchy pages, and takes the occupied nodes at level 7 as the footprint — without downloading a single point. Only nodes at or below the target level count (a COPC keeps a coarse sample at every ancestor, so counting ancestors fills the whole square). Footprints are reprojected from each dataset's CRS with proj4, definitions from epsg.io. France comes out at 33 % of its bounding box; the country is 30 %. About a minute.
Esri Integrated Mesh
ArcGIS Online's public search API, typeKeywords:"IntegratedMesh" AND access:public — the type keyword ArcGIS sets itself on publish, where the free‑text tag finds 48 layers instead of 10 887. Queried one modified year at a time to stay under the 10 000‑item cap, deduplicated by service URL, dropped under 5 km² of declared extent (most of the 10 900 are one drone flight over one building site, and a speck you cannot click is not an answer; that left 1 719), and — because access:public is the item's sharing level, not the service's — every survivor is asked for its own ?f=json anonymously and dropped if it answers with a token error or 403 (75 did). 1 644 footprints, 121 search requests plus one check per service, about two minutes.
Esri has no global photorealistic mesh: its one global 3D Buildings layer (TomTom, Vantor, Community Maps, Overture, quarterly) is modelled buildings, not imagery-derived, and declares a whole-world extent, so there is no footprint to draw and it is not one of these.
Draping custom layers
What MapLibre drapes over its terrain and what it does not, why Matcap and Phong have a raster path and a live GL path, and the tile-seam fix
STAC search internals
How the STAC search tab talks to APIs, static catalogs and the federation, why it uses no STAC client library, and what a stac-geoparquet reader would add