5. Map Tile and Geocoding Request Proxying

Status

Accepted

Date

2026-07-27

Context

Issue #1340 found that the privacy policy claimed personal data is never passed to third parties, while SingleMarkerMap.tsx loaded map tiles directly from tile.openstreetmap.org and useCitySuggestions.ts queried nominatim.openstreetmap.org directly from the browser, both transmitting the visitor’s IP address (and, for Nominatim, the typed search text) straight to the OpenStreetMap Foundation. PR #1409 closed that issue by disclosing the transfers in the privacy policy, but the underlying technical exposure was left in place. Issue #1074 re-flagged the same root cause via a later audit: disclosure alone does not stop the visitor’s IP address from reaching a third party, it only documents that it happens.

Decision

Proxy both request types through the backend so the OpenStreetMap Foundation only ever sees the backend server’s IP address, never the visitor’s. Two new anonymous, rate-limited endpoints were added: GET /v1/maps/tiles/{zoom}/{x}/{y}.png (Api/Maps/GetMapTile) forwards to tile.openstreetmap.org through a new IMapTileService/OpenStreetMapTileService, caching fetched tiles in an in-process IMemoryCache (24h) to reduce repeat upstream requests; GET /v1/maps/cities?q=…​ (Api/Maps/SearchCities) forwards to Nominatim through a new IGeocodingService.SearchCitiesAsync method on the existing NominatimGeocodingService, reusing its established one-request-per-second throttle (see ADR 4). The frontend’s SingleMarkerMap.tsx and useCitySuggestions.ts were changed to call these backend endpoints instead of the third-party hosts directly, and the CSP’s img-src/connect-src no longer need to allow tile.openstreetmap.org/nominatim.openstreetmap.org since the browser no longer talks to them.

Rationale

  • Proxying removes the privacy exposure at its source instead of only disclosing it - no visitor-identifying data (IP address) reaches the OpenStreetMap Foundation for map display or city search.

  • Reusing NominatimGeocodingService’s existing throttle for the new `SearchCitiesAsync method keeps all Nominatim traffic (address geocoding at write time, city search at query time) under a single shared rate limiter, so the app cannot accidentally exceed Nominatim’s public usage policy from two uncoordinated call sites.

  • Caching tiles server-side is required by OpenStreetMap’s tile usage policy for anything beyond light, ad hoc use, and also cuts the number of upstream requests generated by proxying every visitor’s tile loads through one backend.

  • The existing AddHttpClient<TInterface, TImpl> typed-client pattern (used for Keycloak and Nominatim geocoding) extends cleanly to a third external service without introducing a new registration convention.

Considered Alternatives

Disclosure only (status quo after PR #1409)

Keep the privacy policy’s disclosure of the direct browser-to-OpenStreetMap transfers and stop there.

  • Pros

    • No code change, no new backend dependency on OpenStreetMap’s availability

  • Cons

    • Does not address issue #1074’s actual complaint - the visitor’s IP address is still directly exposed to a third party, which is what a privacy-conscious user (and this issue) objects to, not just the absence of a disclosure

    • Leaves an unqualified reliance on legitimate interest (Art. 6(1)(f) GDPR) as the only justification, which is a weaker position than not transferring the data at all

Self-host the tile server and/or a Nominatim instance

Run our own OpenStreetMap tile server and Nominatim instance instead of calling the public ones, proxied or not.

  • Pros

    • Full control over rate limits, caching, and availability; no dependency on the OpenStreetMap Foundation’s public infrastructure

  • Cons

    • Tile rendering requires a multi-hundred-gigabyte planet extract or region extract plus a rendering pipeline (e.g. renderd/mod_tile), and Nominatim needs its own imported database - both far exceed this project’s operational scale and the "open-source self-hosting principle" already weighed in ADR 4

    • Ongoing data-freshness maintenance (periodic re-imports) that a proxy in front of the public services does not need

Frontend-side reverse proxy (CDN/edge rewrite) instead of a backend endpoint

Keep the request client-side but rewrite the host via a CDN or edge function so the visitor’s browser still makes the request, just to a first-party-looking URL that the CDN forwards.

  • Pros

    • Avoids adding load to the application backend

  • Cons

    • The visitor’s IP address is still what the CDN/edge forwards upstream unless the edge itself proxies with its own IP, which most CDN rewrite features do not do by default - so this does not reliably solve the actual privacy problem

    • Introduces a second infrastructure component (the CDN/edge config) for a project that otherwise has no CDN

Consequences

  • Map tiles and city search now depend on the backend being reachable; if the backend is down, the map and city autocomplete degrade along with the rest of the app instead of failing independently.

  • The in-memory tile cache is per-instance and cleared on restart - acceptable at current scale, but if the backend is ever scaled to multiple instances without a shared cache, cache hit rates (and OpenStreetMap’s usage-policy compliance) would need revisiting with a distributed cache.

  • The privacy policy’s OpenStreetMap/Nominatim disclosure subsection (added by PR #1409) now describes the pre-proxy architecture and should be revisited separately to reflect that visitor IP addresses are no longer transmitted to these third parties directly.

  • Adding a third external service integration (alongside Keycloak and Nominatim geocoding) to the same typed-HttpClient pattern keeps Infrastructure/ServiceCollectionExtensions.cs consistent, at the cost of one more outbound dependency the backend must handle failures for gracefully (both new services return null/empty on failure rather than throwing, matching NominatimGeocodingService.GeocodeAsync).