Skip to content

Fully open-source basemap: keyless OSM + Esri reserve with runtime failover (drop CARTO_KEY) - #37

Open
fpietrosanti wants to merge 1 commit into
mxmap:mainfrom
fpietrosanti:feat/keyless-osm-basemap
Open

fpietrosanti wants to merge 1 commit into
mxmap:mainfrom
fpietrosanti:feat/keyless-osm-basemap

Conversation

@fpietrosanti

Copy link
Copy Markdown

Why

CARTO put its raster basemaps behind API keys (watermark rollout 2026-08-28, enforcement 2026-09-23). The current mitigation — a CARTO_KEY GitHub secret sed-injected at deploy — has structural downsides on a static site:

  • the "secret" ships world-readable in the served JS: anyone can copy it and burn the quota (free tier: 5M tiles/month non-commercial, then paid);
  • forks and local checkouts render watermark tiles (__CARTO_KEY__ literal);
  • the deploy hard-fails when the secret is missing;
  • the project takes on a billing relationship for its map background.

This PR proposes going fully open-source and keyless instead. On mxmap.it (the Italian Osservatorio fork of this project) we made this exact choice — OSM instead of private CARTO API keys — and have been running this stack in production since 2026-09-29.

What

Piece Change
Primary basemap OSM standard raster (OSMF community, native z19, slippy /{z}/{x}/{y}). Positron-like light-gray look recreated via CSS grayscale (.basemap-muted)
Labels above polygons OSM bakes labels into the raster → the same tiles are redrawn on a dedicated pane with mix-blend-mode: darken + a midtone-crushing filter: only place names/borders emerge above the data. Same URLs ⇒ browser HTTP cache ⇒ zero extra requests to OSM. @supports guard for old browsers
Runtime failover ≥8 tileerror → automatic swap to the keyless Esri World Light Gray reserve (Base + Reference on the blend pane). Encoded traps: Esri axis order /tile/{z}/{y}/{x} is inverted vs slippy, native max z16 (maxNativeZoom). Manual hook: window.__forceBasemapFailover()
HQ PNG export Template-based tile fetching follows the active basemap; the label pass replicates the blend with canvas globalCompositeOperation='darken'; attribution line updates accordingly
deploy.yml CARTO_KEY injection step removed — no secret needed, forks render exactly what production renders
tests/test_basemap.py Functional battery (7 tests, stdlib+pytest). Structural: keyless-only, axis-order per host, blend-CSS contract, failover wiring, preconnects. Functional: real land tiles (Rome, Milan) for primary and reserve — HTTP 200 + image/* + min size + the anti-placeholder check: two coordinates MUST return different bytes. A watermark is identical everywhere; it is the only check that catches breakage hidden behind HTTP 200 (exactly how the CARTO change broke the map silently)

Verification

  • Battery: 7/7 passed against this branch (and 11/11 of the equivalent battery in mxmap.it CI, where it also runs daily on a schedule to catch provider-side drift ≤24h).
  • E2E in-browser on providers.html from this branch: 32/32 OSM tiles loaded, muted filter and blend pane active (computed styles verified), zero console errors; after __forceBasemapFailover(): 32/32 Esri tiles loaded, 16 Reference label tiles on the blend pane, zero residual OSM tiles.
  • node --check js/map-shared.js clean; ruff format --check + ruff check clean on the new test.

Notes for review

  • OSMF tile usage policy: modest traffic like these pages is in scope; attribution is kept (© OpenStreetMap contributors, also in exports); the label overlay adds no requests (same URLs, cached); no prefetching. If you prefer to keep network tests out of PR CI, the battery can be moved to a scheduled workflow — happy to adjust.
  • Retina: OSM has no keyless {r}/@2x variant — slight sharpness loss on HiDPI vs CARTO. We judged it a fair trade for zero keys/costs; the CSS filter hides most of it.
  • Aesthetic before/after is intentionally close (gray canvas, labels above polygons). Happy to tune the filter values to taste.

🤖 Generated with Claude Code

…o CARTO_KEY

CARTO put its raster basemaps behind API keys (watermark rollout
2026-08-28, enforcement 2026-09-23). The current answer - a CARTO_KEY
GitHub secret injected at deploy time - has structural downsides on a
static site: the key is world-readable in the served JS (anyone can
copy it and burn the quota), forks and local checkouts render watermark
tiles, deploys hard-fail when the secret is missing, and the project
gains a billing dependency.

This PR makes the basemap fully open and keyless:

- Primary: OpenStreetMap standard raster (OSMF, native z19). The
  familiar positron-like light-gray canvas is recreated with a CSS
  grayscale filter (.basemap-muted).
- Labels above polygons: OSM bakes labels into the raster, so the SAME
  tiles are re-drawn on a dedicated pane blended with
  mix-blend-mode:darken + a midtone-crushing filter - only place names
  and admin borders emerge above the data. Same URLs => served from the
  browser HTTP cache, zero extra tile requests to OSM. @supports guard:
  browsers without blending simply hide the overlay.
- Runtime failover: after 8 tileerror events the map swaps to the
  keyless Esri World Light Gray reserve (Base + Reference labels).
  Encoded traps: Esri axis order is /tile/{z}/{y}/{x} (INVERTED vs
  slippy) and native tiles stop at z16 (maxNativeZoom).
- HQ PNG export follows the active basemap (template-based tile
  fetching; the label pass uses canvas globalCompositeOperation=darken
  to match the on-screen look) and updates its attribution line.
- deploy.yml: CARTO_KEY injection step removed - deploys need no
  secret, forks render exactly what production renders.
- tests/test_basemap.py: functional battery. Structural: keyless-only
  templates, axis-order rule per host, blend CSS contract, failover
  wiring, preconnect alignment. Functional: real LAND tiles (Rome,
  Milan) for primary AND reserve - HTTP 200 + image/* + minimum size +
  the anti-placeholder check (two coordinates MUST return different
  bytes; a watermark is identical everywhere - the only check that
  catches breakage hidden behind HTTP 200).

Battle-tested: mxmap.it (the Italian Osservatorio fork) runs exactly
this stack in production since 2026-09-29, with the battery green in CI
and a daily scheduled run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant