// Free OSM Nominatim geocoder — replaces Google Places Autocomplete / // react-geocode across the console. No API key required. Nominatim's usage // policy caps the public endpoint at ~1 req/sec; Astryx's `Typeahead` // debounces queries internally (see `debounceMs` at each call site) and // `createAddressSearchSource` aborts any still-in-flight request whenever a // newer query supersedes it. const NOMINATIM_SEARCH_URL = 'https://nominatim.openstreetmap.org/search'; // Builds a Google-Places-shaped `address_components` array (each entry // `{ long_name, short_name, types: [] }`) from Nominatim's flat `address` // object, so existing call sites' `place.address_components.forEach(...)` // type-switch parsing keeps working unchanged. const buildAddressComponents = (addr = {}) => { const components = []; const push = (longName, types) => { if (longName) components.push({ long_name: longName, short_name: longName, types }); }; push(addr.house_number, ['street_number']); push(addr.road || addr.pedestrian, ['route']); push(addr.suburb || addr.neighbourhood || addr.quarter, ['sublocality_level_1', 'sublocality']); push(addr.city_district || addr.county, ['administrative_area_level_3']); push(addr.city || addr.town || addr.village, ['locality']); push(addr.state, ['administrative_area_level_1']); push(addr.country, ['country']); push(addr.postcode, ['postal_code']); return components; }; // Shapes a Nominatim result close enough to a Google Places `place` object // (name, formatted_address, geometry.location.lat()/lng(), address_components) // that call sites only needed mechanical edits, not rewrites, when swapping // off Google. export const toPlace = (result) => ({ formatted_address: result.display_name, name: result.display_name?.split(',')[0] || '', geometry: { location: { lat: () => parseFloat(result.lat), lng: () => parseFloat(result.lon) } }, address_components: buildAddressComponents(result.address) }); const buildParams = (query, bias) => { const params = new URLSearchParams({ q: query, format: 'json', addressdetails: '1', limit: '5' }); if (bias?.lat && bias?.lng) { const d = 0.5; // ~55km box around the operator's zone — soft bias, not a hard filter params.set('viewbox', `${bias.lng - d},${bias.lat + d},${bias.lng + d},${bias.lat - d}`); } return params; }; // Builds a `SearchSource` for Astryx's `` — // each result is `{ id, label, auxiliaryData }` where `auxiliaryData` is the // `toPlace`-shaped object call sites parse via `address_components`. export function createAddressSearchSource({ bias } = {}) { let controller = null; return { async search(query) { if (!query) return []; controller?.abort(); controller = new AbortController(); try { const params = buildParams(query, bias); const res = await fetch(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, { headers: { Accept: 'application/json' }, signal: controller.signal }); if (!res.ok) return []; const results = await res.json(); return (results || []).map((result) => ({ id: String(result.place_id), label: result.display_name, auxiliaryData: toPlace(result) })); } catch (err) { if (err.name === 'AbortError') return []; console.error('address search error:', err); return []; } }, bootstrap() { return []; }, cancel() { controller?.abort(); } }; }