Map tiles: switch to Esri World Light Gray (OSM 403s file://)

tiles="OpenStreetMap" returned HTTP 403 "referer is required by tile usage
policy" in the target browser — OSM graylists refererless requests and a
karte.html opened via file:// sends no Referer. Esri's Canvas/World_Light_Gray
base + reference layers need neither a key nor a Referer and keep the muted
look. New MAP_TILES / MAP_TILES_LABELS / MAP_TILES_ATTR constants; generate_map
adds them as explicit TileLayers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 22:37:41 +02:00
parent 5b85596a46
commit 5eb60ec2d3
6 changed files with 62 additions and 30 deletions
+18 -5
View File
@@ -61,9 +61,16 @@ GEOCODE_REGIONS = ["Baden-Württemberg", "Hessen"]
MAP_DEFAULT_CENTER = [49.0, 9.0] MAP_DEFAULT_CENTER = [49.0, 9.0]
MAP_DEFAULT_ZOOM = 8 MAP_DEFAULT_ZOOM = 8
#: Kartenkacheln. "OpenStreetMap" braucht keinen API-Schlüssel. #: Kartenkacheln Esri "World Light Gray" (heller, ruhiger Hintergrund).
#: (Die früher genutzten "CartoDB positron"-Kacheln verlangen seit 2024 einen Schlüssel.) #: Braucht keinen API-Schlüssel und keinen Referer (anders als tile.openstreetmap.org,
MAP_TILES = "OpenStreetMap" #: das lokale Dateien mit 403 blockt, und "CartoDB positron", das einen Schlüssel will).
#: Alternativen ohne Schlüssel als Ein-Zeilen-Tausch:
#: Esri Straßenkarte: …/World_Street_Map/MapServer/tile/{z}/{y}/{x}
#: Esri Topo: …/World_Topo_Map/MapServer/tile/{z}/{y}/{x}
_ESRI = "https://server.arcgisonline.com/ArcGIS/rest/services"
MAP_TILES = f"{_ESRI}/Canvas/World_Light_Gray_Base/MapServer/tile/{{z}}/{{y}}/{{x}}"
MAP_TILES_LABELS = f"{_ESRI}/Canvas/World_Light_Gray_Reference/MapServer/tile/{{z}}/{{y}}/{{x}}"
MAP_TILES_ATTR = "Kacheln © Esri"
log = logging.getLogger("arbeitsorte") log = logging.getLogger("arbeitsorte")
@@ -421,9 +428,15 @@ def generate_map(store: DataStore) -> Path:
raise ValueError("Keine Koordinaten vorhanden. Bitte zuerst Einträge speichern.") raise ValueError("Keine Koordinaten vorhanden. Bitte zuerst Einträge speichern.")
m = folium.Map( m = folium.Map(
location=MAP_DEFAULT_CENTER, zoom_start=MAP_DEFAULT_ZOOM, location=MAP_DEFAULT_CENTER, zoom_start=MAP_DEFAULT_ZOOM, tiles=None,
tiles=MAP_TILES,
) )
folium.TileLayer(
MAP_TILES, attr=MAP_TILES_ATTR, name="Karte", control=False,
).add_to(m)
folium.TileLayer(
MAP_TILES_LABELS, attr=MAP_TILES_ATTR, name="Beschriftung",
control=False, overlay=True,
).add_to(m)
max_count = int(freq["count"].max()) max_count = int(freq["count"].max())
for _, row in freq.iterrows(): for _, row in freq.iterrows():
+1 -1
View File
@@ -112,6 +112,6 @@ This is the file to ask for when diagnosing a problem on the user's laptop.
|------------|-----------|-------------------| |------------|-----------|-------------------|
| Nominatim (OSM) | geocoding new/edited entries | entries save with blank coords, shown as `⚠ nicht gefunden` | | Nominatim (OSM) | geocoding new/edited entries | entries save with blank coords, shown as `⚠ nicht gefunden` |
| CDN (jsdelivr, jquery, cloudflare) | rendering `karte.html` | map page loads blank / unstyled | | CDN (jsdelivr, jquery, cloudflare) | rendering `karte.html` | map page loads blank / unstyled |
| OpenStreetMap tiles (`tile.openstreetmap.org`) | map background | blank/grey tiles | | Esri tiles (`server.arcgisonline.com`) | map background | blank/grey tiles |
Existing entries that already have coordinates do **not** need the network. Existing entries that already have coordinates do **not** need the network.
+10
View File
@@ -2,6 +2,16 @@
Notable changes to the app. Newest first. Notable changes to the app. Newest first.
## 2026-09-07 — map tiles → Esri (OSM 403s local files)
The switch to `tiles="OpenStreetMap"` worked on the dev machine but the target
laptop's browser got **HTTP 403 "referer is required by tile usage policy"**
OSM's CDN blocks refererless requests, and a `karte.html` opened as `file://`
sends no `Referer`. Switched to **Esri "World Light Gray"** (base + labels
overlay, `server.arcgisonline.com`): no key, no `Referer` check, muted look
similar to the old positron. Constants `MAP_TILES` / `MAP_TILES_LABELS` /
`MAP_TILES_ATTR`.
## 2026-09-07 — install split for non-admin users ## 2026-09-07 — install split for non-admin users
The target account has no sudo, so install is two scripts: The target account has no sudo, so install is two scripts:
+18 -11
View File
@@ -36,19 +36,26 @@ Numeric parsing is deliberately deferred to `get_map_data` (`pd.to_numeric(...,
errors="coerce")`). Don't "fix" columns to float on load — blank coordinates and errors="coerce")`). Don't "fix" columns to float on load — blank coordinates and
leading-zero PLZ both depend on the string representation. leading-zero PLZ both depend on the string representation.
## CartoDB "positron" tiles now require an API key (2026-09-07) ## Map tiles: Carto needs a key, OSM 403s file:// — use Esri (2026-09-07)
The original `folium.Map(tiles="CartoDB positron")` started rendering an Two dead ends before landing on Esri:
"API KEY REQUIRED / carto.com/basemaps/apikey" watermark Carto moved its
basemaps behind a key. Switched to `tiles="OpenStreetMap"` (constant
`MAP_TILES`), which needs no key. Trade-off: OSM standard tiles are more
colourful than positron's muted grey. Other no-key options if a lighter look is
wanted: OSM Germany (`tile.openstreetmap.de`), OSM France Hot
(`{s}.tile.openstreetmap.fr/hot`) both still colourful. Truly muted
key-free basemaps are hard to find now.
Don't assume a "free" third-party tile source stays free pin the provider in 1. `folium.Map(tiles="CartoDB positron")` → "API KEY REQUIRED" watermark; Carto
one constant so swapping it is a one-line change. moved basemaps behind a key.
2. `tiles="OpenStreetMap"` → worked in `curl` from the dev machine but the
**user's browser got HTTP 403 "referer is required by tile usage policy"**.
OSM's tile CDN graylists refererless requests, and a `karte.html` opened as a
local `file://` sends no `Referer`. A browser can't add one. Dead end for
this app's "open a local HTML file" model.
Now: **Esri "World Light Gray"** base + reference (labels) overlay
(`server.arcgisonline.com/ArcGIS/rest/services/Canvas/…`). No key, no `Referer`
check, light/muted look close to the old positron. `MAP_TILES` /
`MAP_TILES_LABELS` constants; comment lists Esri Street/Topo as swaps.
Lesson: a tile source that works from `curl` or an `http://` page can still fail
from `file://`. Test the map by **opening the generated `karte.html` directly**,
not just by checking the URL in the HTML.
## Nominatim query order matters ## Nominatim query order matters
+5 -5
View File
@@ -163,11 +163,11 @@ or at least skip exact duplicates by default.
## 9. Offline map 🟢 ## 9. Offline map 🟢
`karte.html` loads Leaflet/jQuery/Bootstrap from CDNs and tiles from `karte.html` loads Leaflet/jQuery/Bootstrap from CDNs and tiles from
`tile.openstreetmap.org` (`MAP_TILES`; switched away from CartoDB positron, which `server.arcgisonline.com` (Esri; `MAP_TILES` — Carto needs a key, OSM 403s
now needs an API key). The map is blank without internet even though all the `file://`). The map is blank without internet even though all the data is local.
data is local. If offline use matters, vendor the Leaflet JS/CSS into `data/` If offline use matters, vendor the Leaflet JS/CSS into `data/` and post-process
and post-process the Folium output to point at local files (tiles would still the Folium output to point at local files (tiles would still need caching or an
need caching or an offline tile pack — larger effort). offline tile pack — larger effort).
## 10. Small code-quality items 🟢 — partly done ## 10. Small code-quality items 🟢 — partly done
+10 -8
View File
@@ -65,9 +65,10 @@ A table view of everything in `orte.csv`.
Generated by `generate_map()`: Generated by `generate_map()`:
- Base layer: **OpenStreetMap** standard tiles (`MAP_TILES`) no API key. - Base layer: **Esri "World Light Gray"** (`MAP_TILES` + a labels overlay) a
Initial view centred on `[49.0, 9.0]`, zoom 8 (roughly Baden-Württemberg); light, muted basemap that needs no API key and no `Referer`. Initial view
the view auto-fits to the markers when there is more than one. centred on `[49.0, 9.0]`, zoom 8; auto-fits to the markers when there is more
than one.
- One **`CircleMarker`** per unique `(city, lat, lon)` group. Radius scales - One **`CircleMarker`** per unique `(city, lat, lon)` group. Radius scales
linearly with the visit count for that location linearly with the visit count for that location
(`5 + count / max_count * 20` px), colour is DRK red (`#CC0000`). (`5 + count / max_count * 20` px), colour is DRK red (`#CC0000`).
@@ -77,11 +78,12 @@ Generated by `generate_map()`:
bar shows a warning). bar shows a warning).
> The generated HTML pulls Leaflet, jQuery and Bootstrap from CDNs, and the map > The generated HTML pulls Leaflet, jQuery and Bootstrap from CDNs, and the map
> tiles from `tile.openstreetmap.org`, so the map needs an internet connection to > tiles from `server.arcgisonline.com`, so the map needs an internet connection
> render even though the data is local. To change the look, set `MAP_TILES` near > to render even though the data is local. To change the look, edit `MAP_TILES` /
> the top of `app.py` to another no-key provider Folium knows > `MAP_TILES_LABELS` near the top of `app.py` — the comment there lists other
> (`"OpenStreetMap"`, `"CartoDB positron"` *now needs a key*, or a custom > no-key Esri layers (Street, Topo). `tile.openstreetmap.org` was tried first
> `folium.TileLayer` URL such as OSM Germany / OSM France). > but its tile-usage policy **403s** pages opened from `file://` (no `Referer`);
> `CartoDB positron` needs an API key.
## Geocoding behaviour ## Geocoding behaviour