// FOLLOW-UP: Nominatim responses also include `osm_type` ("node"|"way"|"relation") // and `osm_id` — these form a globally stable, never-reused OSM identity // (e.g. `relation:175905` is New York City forever). We currently discard // them. Capturing them in `extractAddress` as an `osmId` string would give // new gallery records a canonical place ID that's resilient to locality // spelling variants. Only worth doing if the country-alias approach stops // covering the variants we see in `address.country`, since existing PDS // records can't be backfilled and a parallel identity system needs a // reconciliation layer. See server/feeds/location.ts comment. export interface NominatimResult { display_name: string; lat: string; lon: string; type: string; addresstype?: string; name?: string; place_id: number; address?: { amenity?: string; shop?: string; tourism?: string; leisure?: string; house_number?: string; road?: string; city?: string; town?: string; village?: string; state?: string; country?: string; country_code?: string; postcode?: string; }; } const POI_TYPES = new Set(["amenity", "shop", "tourism", "leisure"]); export async function searchLocations(query: string): Promise { if (!query || query.trim().length < 2) return []; const url = new URL("https://nominatim.openstreetmap.org/search"); url.searchParams.set("format", "json"); url.searchParams.set("q", query.trim()); url.searchParams.set("limit", "5"); url.searchParams.set("addressdetails", "1"); try { const response = await fetch(url.toString(), { headers: { "User-Agent": "grain-app/1.0" }, }); if (!response.ok) return []; return await response.json(); } catch { return []; } } export async function reverseGeocode(lat: number, lon: number): Promise { const url = new URL("https://nominatim.openstreetmap.org/reverse"); url.searchParams.set("format", "json"); url.searchParams.set("lat", String(lat)); url.searchParams.set("lon", String(lon)); url.searchParams.set("addressdetails", "1"); try { const response = await fetch(url.toString(), { headers: { "User-Agent": "grain-app/1.0" }, }); if (!response.ok) return null; return await response.json(); } catch { return null; } } /** Returns true if the result is a point-of-interest (venue, shop, etc.) */ export function isPOI(result: NominatimResult): boolean { return POI_TYPES.has(result.addresstype ?? ""); } /** Format the display name — venue/place name for the gallery card. */ export function formatLocationName(result: NominatimResult): string { if (result.name) { return result.name; } return formatGeoContext(result) ?? result.display_name.split(",")[0]; } /** Format geographic context — city/state/country for display. */ export function formatGeoContext(result: NominatimResult): string | undefined { const addr = result.address; if (!addr) return undefined; const parts: string[] = []; const city = addr.city || addr.town || addr.village; if (city) parts.push(city); if (addr.state) parts.push(addr.state); if (addr.country) parts.push(addr.country); return parts.length > 0 ? parts.join(", ") : undefined; } /** Format street address for dropdown disambiguation. */ export function formatStreet(result: NominatimResult): string | undefined { const addr = result.address; if (!addr?.road) return undefined; return addr.house_number ? `${addr.house_number} ${addr.road}` : addr.road; } /** Extract structured address data from a Nominatim result. */ export function extractAddress(result: NominatimResult): { name?: string; street?: string; locality?: string; region?: string; country: string; postalCode?: string; } | null { const addr = result.address; const countryCode = addr?.country_code?.toUpperCase(); if (!countryCode) return null; const name = result.name || undefined; const street = formatStreet(result); const locality = addr?.city || addr?.town || addr?.village || undefined; const region = addr?.state || undefined; const postalCode = addr?.postcode || undefined; return { ...(name ? { name } : {}), ...(street ? { street } : {}), ...(locality ? { locality } : {}), ...(region ? { region } : {}), country: countryCode, ...(postalCode ? { postalCode } : {}), }; }