chore(geocoding): remove Pelias + close 3 bypass paths to public Nominatim

Pelias was retired from the Mac mini on 2026-04-28; photon-self
(self-hosted Photon on mana-gpu) has been the live primary since then.
This removes the now-dead Pelias adapter, config, tests, and the
services/mana-geocoding/pelias/ stack — the entire compose file, the
geojsonify_place_details.js patch, the setup.sh import script.

Provider chain is now `photon-self → photon → nominatim`. The chain
keeps its `privacy: 'local' | 'public'` split, sensitive-query
blocking, coord quantization, and aggressive caching unchanged.

Three direct calls to nominatim.openstreetmap.org that bypassed
mana-geocoding now route through the wrapper:

- citycorners/add-city + citycorners/cities/[slug]/add use the shared
  searchAddress() client (browser → same-origin proxy → mana-geocoding
  → photon-self).
- memoro mobile drops its OSM reverse-geocoding fallback entirely;
  Expo's on-device reverse-geocoding stays as the sole path. Routing
  through the wrapper would require a memoro-server proxy endpoint —
  a follow-up if Expo's quality proves insufficient.

Other behavioral changes:

- CACHE_PUBLIC_TTL_MS dropped from 7d → 1h. The long TTL was a
  privacy-amplification trick from the Pelias era; with photon-self
  serving the bulk of traffic, a transient cross-LAN blip was pinning
  cached fallback answers for days. 1h gives quick recovery.
- /health/pelias renamed to /health/photon-self; prometheus blackbox
  config + status-page generator updated.
- mana-geocoding container no longer needs `extra_hosts:
  host.docker.internal:host-gateway` (was only there for the
  Pelias-on-host-network era).

113 tests passing. CLAUDE.md rewritten to reflect the post-Pelias
architecture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-28 22:12:26 +02:00
parent 7bca16dfa7
commit 2bbcf14aba
35 changed files with 330 additions and 1262 deletions

View file

@ -21,10 +21,10 @@ interface CachedAnswer {
}
/**
* TTL chooser. Public-API results (Photon/Nominatim) get the longer TTL
* caching aggressively is the main privacy lever once the query has
* already left our network. Local results stay on the shorter TTL because
* the Pelias index can be re-imported; we don't want stale local data.
* TTL chooser. Public-API results (photon/nominatim) get a shorter TTL
* (1h) so a transient blip in photon-self doesn't pin stale fallback
* answers in the cache for days. Local results (photon-self) get the
* longer 24h TTL.
*
* Sensitive-query notices are cached on the short TTL too (the user might
* retry from a different angle quickly), and `undefined` provider (chain

View file

@ -9,35 +9,43 @@ export function createHealthRoutes(config: Config, chain: ProviderChain) {
app.get('/', (c) => c.json({ status: 'ok', service: 'mana-geocoding' }));
/**
* Upstream Pelias health. Proxies a request to the Pelias API so
* monitoring can reach it without `extra_hosts: host.docker.internal`
* on the blackbox exporter.
* Upstream photon-self health. Proxies a request to the self-hosted
* Photon so monitoring can reach it without `extra_hosts:
* host.docker.internal` on the blackbox exporter.
*
* Backwards-compatible: existing prometheus probes against this
* endpoint keep working. Now reports `degraded` (200) instead of `down`
* (503) when Pelias is unreachable but a fallback provider is healthy
* the system can still serve queries, just slower / less precise.
* Reports `degraded` (200) instead of `down` (503) when photon-self is
* unreachable but a public fallback (photon / nominatim) is healthy
* the system can still serve queries, just at the cost of leaking the
* query content to a third party.
*/
app.get('/pelias', async (c) => {
app.get('/photon-self', async (c) => {
const upstream = config.photonSelf.apiUrl;
if (!upstream) {
return c.json({ status: 'unconfigured', error: 'PHOTON_SELF_API_URL is unset' }, 503);
}
try {
const res = await fetch(`${config.pelias.apiUrl}/status`, {
const res = await fetch(`${upstream}/api?q=Konstanz&limit=1`, {
signal: AbortSignal.timeout(5000),
});
if (!res.ok && res.status !== 404) {
if (!res.ok) {
return c.json(
{ status: 'degraded', upstream: res.status, fallbackAvailable: chainHasFallback(chain) },
chainHasFallback(chain) ? 200 : 503
{
status: 'degraded',
upstream: res.status,
fallbackAvailable: chainHasPublicFallback(chain),
},
chainHasPublicFallback(chain) ? 200 : 503
);
}
return c.json({ status: 'ok', upstream: 'pelias-api' });
return c.json({ status: 'ok', upstream: 'photon-self' });
} catch (e) {
return c.json(
{
status: chainHasFallback(chain) ? 'degraded' : 'down',
status: chainHasPublicFallback(chain) ? 'degraded' : 'down',
error: e instanceof Error ? e.message : 'unknown',
fallbackAvailable: chainHasFallback(chain),
fallbackAvailable: chainHasPublicFallback(chain),
},
chainHasFallback(chain) ? 200 : 503
chainHasPublicFallback(chain) ? 200 : 503
);
}
});
@ -56,10 +64,10 @@ export function createHealthRoutes(config: Config, chain: ProviderChain) {
}
/**
* Check if any non-Pelias provider is currently believed healthy. Used
* to soften /pelias health to "degraded" instead of "down" when a
* fallback can still serve traffic.
* Check if any public fallback provider is currently believed healthy.
* Used to soften /photon-self health to "degraded" instead of "down"
* when a public fallback can still serve traffic.
*/
function chainHasFallback(chain: ProviderChain): boolean {
return chain.getHealthSnapshot().some((p) => p.name !== 'pelias' && p.healthy);
function chainHasPublicFallback(chain: ProviderChain): boolean {
return chain.getHealthSnapshot().some((p) => p.name !== 'photon-self' && p.healthy);
}