Seit heute läuft unter /karte/ eine interaktive Karte der Region München, komplett von diesem Server. Kein Google, keine osm.org-Tiles, keine einzige Anfrage an Dritte. Der klassische Weg dorthin wäre ein Kartenserver gewesen: PostGIS-Datenbank, Renderer, Update-Pipeline. Gebaut haben wir stattdessen eine einzige Datei.
Eine Datei, 18.248 Kacheln
Das Format heißt PMTiles: alle Vektor-Kacheln einer Region, sortiert und indiziert in einem Archiv. Der Browser holt sich per HTTP-Range-Request genau die Bytes der Kacheln, die der sichtbare Ausschnitt braucht. Serverseitig genügt ein Webspace, der Teilabfragen beantwortet.
Die Datei selbst mussten wir nicht einmal bauen. Protomaps veröffentlicht
täglich einen Planet-Build, und das pmtiles-CLI schneidet daraus per
Range-Requests eine Region heraus, ohne den Planeten herunterzuladen:
pmtiles extract https://build.protomaps.com/20260919.pmtiles \
muenchen.pmtiles --bbox=10.9,47.7,12.1,48.6
Ergebnis nach 27 Sekunden: 157 MB, 18.248 Kacheln, Landsberg bis Wasserburg,
Weilheim bis Freising. Im Browser rendert
MapLibre GL (v5.24), den Stil liefert
@protomaps/basemaps mit deutschen Labels und einem Light- und
Dark-Flavor. Schriften und Sprites liegen als Kopie im Projekt, damit zur
Laufzeit kein CDN angefragt wird. Monatliches Update ist ein Cron mit
demselben Extract-Befehl.
Falle eins: MapLibre v6 schweigt
npm install maplibre-gl holte v6, und die Karte blieb grau. Kein Fehler,
keine Konsole, kein error-Event. styleLoaded: false, null Tile-Requests,
sonst nichts.
Die Ursache steht im Changelog, nur eben nicht im Fehlerbild: v6 lädt
Kacheln in einem Worker, und ein mit addProtocol auf dem Main-Thread
registriertes Protokoll, so wie es die pmtiles-Bibliothek seit Jahren
dokumentiert, existiert in diesem Worker schlicht nicht. Dafür gibt es
jetzt importScriptInWorkers. Wir sind auf v5 zurück, das ist der überall
erprobte Pfad, und haben den Grund ins Repo geschrieben, damit das nächste
Update nicht dieselbe Stunde kostet.
Falle zwei: gzip frisst Range-Requests
Lokal lief alles, deployed war die Karte wieder grau. Diesmal war der Header verräterisch:
content-type: text/plain
etag: W/"9d4fdbb-…;gz"
Der Webserver kennt die Endung .pmtiles nicht, liefert die Datei als
Text aus und komprimiert sie unterwegs. Eine on-the-fly komprimierte
Antwort hat aber keine stabilen Byte-Positionen mehr, also beantwortet der
Server Range-Requests mit der vollen Datei: Status 200 statt 206. Der
Browser jedes Besuchers hätte 157 MB geladen. Der Fix ist eine
.htaccess-Zeile, die die Datei zum Binärformat erklärt, plus abgeschaltete
Kompression; komprimieren lohnt ohnehin nicht, die Kacheln sind intern
schon gzip.
Bemerkenswert fanden wir das Symptom-Paar: zwei völlig verschiedene Ursachen, beide Male dieselbe stumme graue Fläche. Kartenbibliotheken scheitern leise.
Der Knopf davor
Auf den Kontaktseiten (hier und drüben bei pad systems) steckt die Karte hinter einem statischen Vorschaubild und lädt erst auf Klick. Das ist keine Datenschutz-Geste, es gibt ja nichts einzuwilligen. Es sind schlicht ~300 KB JavaScript plus WebGL-Kontext, die eine Kontaktseite nicht tragen muss, solange niemand die Karte will. Das Vorschaubild ist ein Playwright-Screenshot der echten Karte, 41 KB.
Die Kacheldatei liegt übrigens nur einmal, auf diesem Host. Die
pad-systems-Seite bettet sie per iframe ein; eine frame-ancestors-Regel
erlaubt genau das den eigenen Domains. Dieselbe Datei könnte so beliebig
viele Sites versorgen.
Pflicht bleibt die Attribution „© OpenStreetMap-Mitwirkende”, die Daten stehen unter ODbL. Und wer Adresssuche oder Routing erwartet: beides ist absichtlich nicht drin, das sind eigene Systeme. Für „wo ist das Gebäude” reicht eine Datei.