// Free, keyless forward geocoding via OpenStreetMap Nominatim. // // Usage policy — https://operations.osmfoundation.org/policies/nominatim/ : // • max 1 request/second, no heavy bulk use // • an identifying User-Agent is REQUIRED (set GEOCODER_USER_AGENT to a // contact URL/email in production) // • results must be cached — we persist latitude/longitude on the property, // so each address is geocoded once on save, never on map render. // // Best-effort by design: every failure path returns null and the caller simply // proceeds without coordinates (the property is omitted from the map). const NOMINATIM_URL = "https://nominatim.openstreetmap.org/search" const USER_AGENT = process.env.GEOCODER_USER_AGENT || `PropertyManagementNetwork/1.0 (${process.env.NEXT_PUBLIC_APP_URL || "https://propertymanagement.network"})` export type Coordinates = { latitude: number; longitude: number } export type AddressParts = { address_line1?: string | null address_line2?: string | null city?: string | null state?: string | null postal_code?: string | null country?: string | null } /** True when there's enough of an address to bother geocoding. */ export function hasGeocodableAddress(a: AddressParts): boolean { return Boolean(a.address_line1 || a.city || a.postal_code) } export async function geocodeAddress(a: AddressParts): Promise { if (!hasGeocodableAddress(a)) return null const street = [a.address_line1, a.address_line2].filter(Boolean).join(" ").trim() const params = new URLSearchParams({ format: "jsonv2", limit: "1", addressdetails: "0" }) if (street) params.set("street", street) if (a.city) params.set("city", a.city) if (a.state) params.set("state", a.state) if (a.postal_code) params.set("postalcode", a.postal_code) params.set("country", a.country || "US") const controller = new AbortController() const timer = setTimeout(() => controller.abort(), 8000) try { const res = await fetch(`${NOMINATIM_URL}?${params.toString()}`, { headers: { "User-Agent": USER_AGENT, Accept: "application/json" }, signal: controller.signal, }) if (!res.ok) return null const json = (await res.json()) as Array<{ lat: string; lon: string }> const first = Array.isArray(json) ? json[0] : undefined if (!first) return null const latitude = Number(first.lat) const longitude = Number(first.lon) if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null return { latitude, longitude } } catch { return null } finally { clearTimeout(timer) } }