Terrain Viewer
Dev

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

GroupGeometrySource
MapterhornVector tiles, per national sourceMapterhorn coverage tiles (single-archive-tiles.mapterhorn.com/coverage/{z}/{x}/{y}.mvt) + attribution.json
Terrain / basemap libraryBounds rectanglesbounds of each entry in lib/custom-sources.json
OSM Editor Layer IndexCoverage polygons@osm-editor-kit/maplibre-editor-layer-index (bundled index, bumped weekly)
Your sourcesBounds rectanglesthe bounds you declared in the source dialog
Bing Maps 3DMerged tile rectangles, ~2.4 kmBing'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 3DDecoded polygons, dissolved, ~500 mGoogle’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 LiDARTraced octree cells, per datasetEach 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 MeshOne rectangle per scene layerArcGIS 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 cloudsDataset footprints (outlines where the catalog has them, boxes otherwise), ~100 mOpenTopography'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

Bing, Google, Esri Integrated Mesh and FLAI coverage over Europe

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:

  1. 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/.
  2. 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 %.
  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. Output public/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.

On this page