Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 6x 6x 3x 5x 5x 3x 3x 5x 6x 6x 1x 1x 1x 1x 1x 2x 2x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 1x 1x 1x 1x 1x 1x 2x 2x 5x 3x 1x 1x 3x 1x 1x 1x 5x 5x 5x 5x | import { AppError } from "@ontrack/backend-common";
export type NominatimConfig = {
baseUrl: string;
userAgent: string;
timeoutMs?: number;
};
export type ReverseGeocodeResult = {
locality: string;
};
type NominatimAddress = {
city?: string;
town?: string;
village?: string;
municipality?: string;
county?: string;
};
type NominatimReverseResponse = {
display_name?: string;
address?: NominatimAddress;
};
const ADDRESS_KEYS: (keyof NominatimAddress)[] = [
"city",
"town",
"village",
"municipality",
"county",
];
const DEFAULT_TIMEOUT_MS = 5_000;
export function extractLocalityFromNominatimResponse(data: NominatimReverseResponse): string {
const address = data.address;
if (address) {
for (const key of ADDRESS_KEYS) {
const value = address[key]?.trim();
if (value) {
return value;
}
}
}
const displayName = data.display_name?.trim();
if (displayName) {
const firstSegment = displayName.split(",")[0]?.trim();
if (firstSegment) {
return firstSegment;
}
}
return "";
}
export async function reverseGeocode(
lat: number,
lon: number,
config: NominatimConfig,
): Promise<ReverseGeocodeResult> {
const baseUrl = config.baseUrl.replace(/\/$/, "");
const url = new URL(`${baseUrl}/reverse`);
url.searchParams.set("lat", String(lat));
url.searchParams.set("lon", String(lon));
url.searchParams.set("format", "json");
const controller = new AbortController();
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
method: "GET",
headers: {
"User-Agent": config.userAgent,
Accept: "application/json",
},
signal: controller.signal,
});
if (!response.ok) {
throw new AppError(
502,
"NOMINATIM_UPSTREAM_ERROR",
`Nominatim responded with status ${response.status}`,
);
}
const data = (await response.json()) as NominatimReverseResponse;
return { locality: extractLocalityFromNominatimResponse(data) };
} catch (error) {
if (error instanceof AppError) {
throw error;
}
if (error instanceof Error && error.name === "AbortError") {
throw new AppError(502, "NOMINATIM_TIMEOUT", "Nominatim request timed out");
}
throw new AppError(502, "NOMINATIM_UNAVAILABLE", "Nominatim request failed");
} finally {
clearTimeout(timeout);
}
}
|