mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-18 19:41:23 +02:00
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:
parent
7bca16dfa7
commit
2bbcf14aba
35 changed files with 330 additions and 1262 deletions
|
|
@ -1,9 +1,11 @@
|
|||
/**
|
||||
* Shared geocoding client for all modules in the unified Mana app.
|
||||
*
|
||||
* Talks to our self-hosted mana-geocoding service (Pelias-backed, port 3018).
|
||||
* All queries stay within our infrastructure — no user location data leaves
|
||||
* the network.
|
||||
* Talks to mana-geocoding (port 3018), which fronts a provider chain
|
||||
* (photon-self → public photon → public nominatim) with sensitive-query
|
||||
* blocking and coord quantization. Sensitive + happy-path queries stay
|
||||
* on our infrastructure via photon-self; only last-resort fallbacks
|
||||
* leave the network.
|
||||
*
|
||||
* Used by: places, events, contacts, photos, …
|
||||
*
|
||||
|
|
@ -66,26 +68,24 @@ export interface GeocodingResult {
|
|||
longitude: number;
|
||||
address: GeocodingAddress;
|
||||
category: PlaceCategory;
|
||||
/** Raw Pelias categories (food, retail, transport, …) — only present
|
||||
* when the result came from Pelias. */
|
||||
peliasCategories?: string[];
|
||||
confidence: number;
|
||||
/** Which backend served this result. `pelias` is local; `photon` and
|
||||
* `nominatim` are public APIs (the wrapper applies sensitive-query
|
||||
* blocking + coord quantization before forwarding to those). */
|
||||
provider?: 'pelias' | 'photon' | 'nominatim';
|
||||
/** Which backend served this result. `photon-self` is our self-hosted
|
||||
* Photon (privacy: 'local'); `photon` and `nominatim` are public APIs
|
||||
* (the wrapper applies sensitive-query blocking + coord quantization
|
||||
* before forwarding to those). */
|
||||
provider?: 'photon-self' | 'photon' | 'nominatim';
|
||||
}
|
||||
|
||||
/**
|
||||
* Out-of-band information returned alongside results — the wrapper uses
|
||||
* this to signal *why* a query had unusual behavior:
|
||||
*
|
||||
* - `'fallback_used'`: Pelias was unreachable, so a public-API provider
|
||||
* served the request. Results are still valid but may be less precise.
|
||||
* UI should show a subtle "approximate" badge.
|
||||
* - `'fallback_used'`: photon-self was unreachable, so a public-API
|
||||
* provider served the request. Results are still valid but may be
|
||||
* less precise. UI should show a subtle "approximate" badge.
|
||||
* - `'sensitive_local_unavailable'`: the query matched the wrapper's
|
||||
* sensitive-keyword list (medical / mental-health / crisis service)
|
||||
* AND the local Pelias was unreachable. The wrapper deliberately did
|
||||
* AND no local provider was reachable. The wrapper deliberately did
|
||||
* NOT forward the query to public APIs. Results are empty by design.
|
||||
* UI should explain this to the user.
|
||||
*/
|
||||
|
|
@ -95,7 +95,7 @@ interface GeocodingResponse {
|
|||
results: GeocodingResult[];
|
||||
cached?: boolean;
|
||||
error?: string;
|
||||
provider?: 'pelias' | 'photon' | 'nominatim';
|
||||
provider?: 'photon-self' | 'photon' | 'nominatim';
|
||||
notice?: GeocodingNotice;
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ interface GeocodingResponse {
|
|||
*/
|
||||
export interface SearchOutcome {
|
||||
results: GeocodingResult[];
|
||||
provider?: 'pelias' | 'photon' | 'nominatim';
|
||||
provider?: 'photon-self' | 'photon' | 'nominatim';
|
||||
notice?: GeocodingNotice;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { cityTable, useAllCities } from '$lib/modules/citycorners';
|
||||
import type { LocalCity } from '$lib/modules/citycorners/types';
|
||||
import { RoutePage } from '$lib/components/shell';
|
||||
import { searchAddress } from '$lib/geocoding';
|
||||
|
||||
const allCities = useAllCities();
|
||||
|
||||
|
|
@ -44,14 +45,10 @@
|
|||
geocoding = true;
|
||||
try {
|
||||
const searchQ = country.trim() ? `${q}, ${country.trim()}` : q;
|
||||
const res = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(searchQ)}&limit=1`,
|
||||
{ headers: { 'User-Agent': 'CityCorners/1.0' } }
|
||||
);
|
||||
const results = await res.json();
|
||||
const results = await searchAddress(searchQ, { limit: 1 });
|
||||
if (results.length > 0) {
|
||||
latitude = parseFloat(results[0].lat);
|
||||
longitude = parseFloat(results[0].lon);
|
||||
latitude = results[0].latitude;
|
||||
longitude = results[0].longitude;
|
||||
}
|
||||
} catch {
|
||||
// best-effort
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import { ccLocationTable, CATEGORY_KEYS } from '$lib/modules/citycorners';
|
||||
import type { LocalCity, LocalLocation } from '$lib/modules/citycorners/types';
|
||||
import { RoutePage } from '$lib/components/shell';
|
||||
import { searchAddress } from '$lib/geocoding';
|
||||
|
||||
const cityCtx = getContext<{ value: LocalCity | undefined }>('currentCity');
|
||||
let city = $derived(cityCtx.value);
|
||||
|
|
@ -58,14 +59,10 @@
|
|||
cityName && !addr.toLowerCase().includes(cityName.toLowerCase())
|
||||
? `${addr}, ${cityName}`
|
||||
: addr;
|
||||
const res = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(q)}&limit=1`,
|
||||
{ headers: { 'User-Agent': 'CityCorners/1.0' } }
|
||||
);
|
||||
const results = await res.json();
|
||||
const results = await searchAddress(q, { limit: 1 });
|
||||
if (results.length > 0) {
|
||||
latitude = parseFloat(results[0].lat);
|
||||
longitude = parseFloat(results[0].lon);
|
||||
latitude = results[0].latitude;
|
||||
longitude = results[0].longitude;
|
||||
}
|
||||
} catch {
|
||||
// Geocoding is best-effort
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@
|
|||
* If we ever want to rate-limit by user we can add JWT verification here
|
||||
* without touching the upstream service.
|
||||
*
|
||||
* Also proxies /health and /health/pelias so the SvelteKit status page
|
||||
* (/status) can check the service from its server side.
|
||||
* Also proxies /health and /health/photon-self so the SvelteKit status
|
||||
* page (/status) can check the service from its server side.
|
||||
*/
|
||||
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
|
|
|||
|
|
@ -155,71 +155,26 @@ export const reverseGeocodeWithExpo = async (
|
|||
};
|
||||
|
||||
/**
|
||||
* Führt ein Reverse Geocoding mit OpenStreetMap/Nominatim durch
|
||||
* @param latitude Breitengrad
|
||||
* @param longitude Längengrad
|
||||
* @returns Adressinformationen oder null bei Fehler
|
||||
*/
|
||||
export const reverseGeocodeWithOSM = async (
|
||||
latitude: number,
|
||||
longitude: number
|
||||
): Promise<AddressInfo | null> => {
|
||||
try {
|
||||
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&addressdetails=1`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Memoro App', // OSM erfordert einen User-Agent
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OSM API responded with status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.address) {
|
||||
return {
|
||||
street: data.address.road || data.address.pedestrian || data.address.street,
|
||||
streetNumber: data.address.house_number,
|
||||
postalCode: data.address.postcode,
|
||||
city: data.address.city || data.address.town || data.address.village,
|
||||
district: data.address.suburb || data.address.neighbourhood,
|
||||
region: data.address.state,
|
||||
country: data.address.country,
|
||||
name: data.name,
|
||||
formattedAddress: data.display_name,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.debug('Fehler beim Reverse Geocoding mit OSM:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Führt ein Reverse Geocoding durch und versucht, die beste verfügbare Adresse zu ermitteln
|
||||
* @param latitude Breitengrad
|
||||
* @param longitude Längengrad
|
||||
* @returns Adressinformationen oder null bei Fehler
|
||||
* Führt ein Reverse Geocoding durch. Nutzt ausschließlich Expo's
|
||||
* On-Device Reverse-Geocoding — keine direkten Calls an
|
||||
* nominatim.openstreetmap.org, weil das die User-IP + Coords ungeschützt
|
||||
* an einen Public-Service leakt. Wenn Expo keine Adresse liefert,
|
||||
* geben wir null zurück.
|
||||
*
|
||||
* Falls Expo's Qualität auf Dauer nicht reicht, ist der richtige Fix
|
||||
* ein Proxy-Endpoint im memoro-server, der intern an mana-geocoding
|
||||
* weiterreicht (Privacy-Hardening + Photon-Self).
|
||||
*/
|
||||
export const getAddressFromCoordinates = async (
|
||||
latitude: number,
|
||||
longitude: number
|
||||
): Promise<AddressInfo | null> => {
|
||||
try {
|
||||
// Zuerst mit Expo versuchen
|
||||
const expoResult = await reverseGeocodeWithExpo(latitude, longitude);
|
||||
|
||||
// Wenn Expo ein gutes Ergebnis liefert, dieses verwenden
|
||||
if (expoResult && expoResult.street && expoResult.city) {
|
||||
return expoResult;
|
||||
}
|
||||
|
||||
// Ansonsten mit OSM versuchen
|
||||
return await reverseGeocodeWithOSM(latitude, longitude);
|
||||
return expoResult;
|
||||
} catch (error) {
|
||||
console.debug('Fehler beim Reverse Geocoding:', error);
|
||||
return null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue