import { isGothenburgDeliveryAddress } from '@/domain/delivery/gothenburg-address'; import { disableGeoapifyAutocomplete, isGeoapifyApiKeyConfigured, isGeoapifyAutocompleteDisabled, mapGeoapifyHttpStatus, type GeoapifyUnavailableReason, } from './geoapify-availability'; import { getCachedGeoapifyFeatures, getInFlightGeoapifyRequest, setCachedGeoapifyFeatures, trackInFlightGeoapifyRequest, waitForGeoapifyRateLimit, } from './geoapify-request-cache'; import type { GeoapifyAutocompleteResponse, GeoapifyFeature } from './geoapify-types'; /** Backaplan branch — bias autocomplete toward Gothenburg. */ const GOTHENBURG_BIAS = 'proximity:11.944,57.699'; /** Approximate Göteborg municipality bounding box (minLon,minLat,maxLon,maxLat). */ const GOTHENBURG_RECT_FILTER = 'rect:11.74,57.60,12.15,57.82'; export type GeoapifyAutocompleteResult = | { status: 'ok'; features: GeoapifyFeature[] } | { status: 'unavailable'; reason: GeoapifyUnavailableReason; features: [] }; export function isDeliverableGeoapifyFeature(feature: GeoapifyFeature): boolean { const { properties } = feature; return isGothenburgDeliveryAddress({ city: properties.city, municipality: properties.municipality, postcode: properties.postcode, countryCode: properties.country_code, formatted: properties.formatted, }); } export function shouldUseGeoapifyAutocomplete(): boolean { return isGeoapifyApiKeyConfigured() && !isGeoapifyAutocompleteDisabled(); } function unavailable(reason: GeoapifyUnavailableReason): GeoapifyAutocompleteResult { disableGeoapifyAutocomplete(); return { status: 'unavailable', reason, features: [] }; } async function requestGeoapifyAddressSuggestions( query: string, lang: 'sv' | 'en', ): Promise { const cached = getCachedGeoapifyFeatures(query, lang); if (cached) return cached; const inFlight = getInFlightGeoapifyRequest(query, lang); if (inFlight) return inFlight; const request = (async () => { await waitForGeoapifyRateLimit(); const apiKey = process.env.NEXT_PUBLIC_GEOAPIFY_API_KEY!.trim(); const params = new URLSearchParams({ text: query.trim(), format: 'geojson', lang, limit: '8', filter: `countrycode:se|${GOTHENBURG_RECT_FILTER}`, bias: GOTHENBURG_BIAS, apiKey, }); const response = await fetch( `https://api.geoapify.com/v1/geocode/autocomplete?${params.toString()}`, ); if (!response.ok) { throw new GeoapifyRequestError(mapGeoapifyHttpStatus(response.status)); } const data = (await response.json()) as GeoapifyAutocompleteResponse; const features = (data.features ?? []).filter(isDeliverableGeoapifyFeature); setCachedGeoapifyFeatures(query, lang, features); return features; })(); return trackInFlightGeoapifyRequest(query, lang, request); } class GeoapifyRequestError extends Error { constructor(public readonly reason: GeoapifyUnavailableReason) { super(reason); this.name = 'GeoapifyRequestError'; } } export async function fetchGeoapifyAddressSuggestions( query: string, lang: 'sv' | 'en' = 'sv', ): Promise { if (!isGeoapifyApiKeyConfigured()) { return unavailable('missing_key'); } if (isGeoapifyAutocompleteDisabled()) { return unavailable('quota_exceeded'); } if (query.trim().length < 3) { return { status: 'ok', features: [] }; } try { const features = await requestGeoapifyAddressSuggestions(query, lang); return { status: 'ok', features }; } catch (error) { if (error instanceof GeoapifyRequestError) { return unavailable(error.reason); } return unavailable('network'); } }