pad.software
Kontakt

Blog · Werkstatt

Ein Kartendienst ohne Kartenserver

Wie die Anfahrtskarte auf /karte/ entstanden ist: ein 157-MB-Regionsauszug als statische Datei, MapLibre im Browser, und zwei Fallen, die beide dasselbe Symptom hatten: eine stumme graue Karte.

3 Min. LesezeitAuf LinkedIn teilen

  • pmtiles
  • maplibre
  • openstreetmap
  • hosting

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.

Lieber direkt fragen?

Antwort innerhalb eines Werktages.