121 lines
3.6 KiB
TypeScript
121 lines
3.6 KiB
TypeScript
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<GeoapifyFeature[]> {
|
|
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<GeoapifyAutocompleteResult> {
|
|
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');
|
|
}
|
|
} |