From 1f68ba1ea15f169324966e3a8a832b99a081ac07 Mon Sep 17 00:00:00 2001 From: Maurice Date: Mon, 13 Apr 2026 20:16:36 +0200 Subject: [PATCH] fix(atlas): prevent Nominatim 429 rate limiting (#576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Swap resolve order: try local bbox lookup before Nominatim reverse geocode — eliminates most external API calls - Add global throttling (1.1s min between requests) to reverseGeocodeCountry so /stats can't flood Nominatim - Update User-Agent header to include repo URL per Nominatim policy --- server/src/services/atlasService.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/server/src/services/atlasService.ts b/server/src/services/atlasService.ts index 2f38837c..1c4f06da 100644 --- a/server/src/services/atlasService.ts +++ b/server/src/services/atlasService.ts @@ -171,12 +171,19 @@ export const CONTINENT_MAP: Record = { // ── Geocoding helpers ─────────────────────────────────────────────────────── +let lastNominatimCall = 0; + export async function reverseGeocodeCountry(lat: number, lng: number): Promise { const key = roundKey(lat, lng); if (geocodeCache.has(key)) return geocodeCache.get(key)!; + // Nominatim rate limit: max 1 req/sec + const now = Date.now(); + const elapsed = now - lastNominatimCall; + if (elapsed < 1100) await new Promise(r => setTimeout(r, 1100 - elapsed)); + lastNominatimCall = Date.now(); try { const res = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&zoom=3&accept-language=en`, { - headers: { 'User-Agent': 'TREK Travel Planner' }, + headers: { 'User-Agent': 'TREK Travel Planner (https://github.com/mauriceboe/TREK)' }, }); if (!res.ok) return null; const data = await res.json() as { address?: { country_code?: string } }; @@ -215,15 +222,15 @@ export function getCountryFromAddress(address: string | null): string | null { return null; } -// ── Resolve a place to a country code (address -> geocode -> bbox) ────────── +// ── Resolve a place to a country code (address -> bbox -> geocode) ────────── async function resolveCountryCode(place: Place): Promise { let code = getCountryFromAddress(place.address); if (!code && place.lat && place.lng) { - code = await reverseGeocodeCountry(place.lat, place.lng); + code = getCountryFromCoords(place.lat, place.lng); } if (!code && place.lat && place.lng) { - code = getCountryFromCoords(place.lat, place.lng); + code = await reverseGeocodeCountry(place.lat, place.lng); } return code; }