keep bent streets, enclosed landmarks and same-name outlines honest

A street ribbon that holds neither its ring mean nor the anchor is centred
on the street's nearest point when the street runs within 100 m.

Nominatim outline selection ranks an exact name, then one holding every
asked word, then an overlap, and refuses an overlap whose name lacks the
ask's last word unless that word is in the result's address or type, so
"Central Park Zoo" is no longer Central Park. The server caches every
accepted outline and answers each ask with the one holding its resolved
anchor (sent as alat/alon), read across the antimeridian.

A duplicate mark that fails to clean up no longer tears down the new
outline it duplicates.
This commit is contained in:
Sameh Khamis committed 2026-11-02 08:37:45 -08:00
1 parent d9a460658d
commit a38a065c5c
16 files changed
+516 -133

No files matched your search

+31 -13
View File
@@ -19,6 +19,7 @@ import {
import {
OUTLINE_KINDS,
OUTLINE_POLYGON_THRESHOLD,
outlineAt,
outlineBias,
selectOutlineResult,
} from './outlineSelect.js';
@@ -52,6 +53,11 @@ function requestKey(gate, kind, params) {
return `${CACHE_SCHEMA}|${endpoint}|${kind}|${params}`;
}
/** A decimal coordinate query value, or NaN: never "" or " " as 0. */
function coordinateParam(value) {
return /^-?\d+(\.\d+)?$/.test(value ?? '') ? Number(value) : NaN;
}
function malformed() {
return Object.assign(new Error('Place answer was not understood'), {
code: 'NOMINATIM_MALFORMED',
@@ -222,8 +228,10 @@ export function createNominatimSearchProvider({
/**
* Construct the outline lookup: one guarded search with polygons for one
* explicit ask, accepting only a result whose class suits the ask kind.
* Found outlines are kept 90 days and genuine no-results 24 hours.
* explicit ask, accepting only results whose class suits the ask kind.
* Every accepted result is kept (90 days; genuine no-results 24 hours), and
* each ask is answered with the one holding its anchor, so a same-name place
* elsewhere neither answers nor repeats from the cache.
*/
export function createNominatimOutlineProvider({
gate = () => sharedNominatimGate(),
@@ -232,8 +240,15 @@ export function createNominatimOutlineProvider({
const inFlight = new Map();
const currentGate = () => (typeof gate === 'function' ? gate() : gate);
const answer = (payload, anchorLat, anchorLon, cached) => ({
status: payload.status,
outline: outlineAt(payload.outlines, anchorLat, anchorLon),
...(payload.skipped ? { skipped: payload.skipped } : {}),
...(cached ? { cached: true } : {}),
});
return async function fetchNominatimOutline(
{ query, kind, lat, lon },
{ query, kind, lat, lon, anchorLat = null, anchorLon = null },
{ signal } = {},
) {
const bias = outlineBias(kind, lat, lon);
@@ -249,10 +264,10 @@ export function createNominatimOutlineProvider({
});
if (bias) params.set('viewbox', bias.viewbox);
const gateNow = currentGate();
const cacheKey = requestKey(gateNow, `outline-${kind}`, params);
const cacheKey = requestKey(gateNow, `outlines-${kind}`, params);
const cached = await cache.get(cacheKey);
if (cached) return { ...cached.payload, cached: true };
return await subscribeSharedRequest(
if (cached) return answer(cached.payload, anchorLat, anchorLon, true);
const payload = await subscribeSharedRequest(
inFlight,
cacheKey,
signal,
@@ -262,23 +277,24 @@ export function createNominatimOutlineProvider({
{ signal: sharedSignal, maxBytes: OUTLINE_MAX_RESPONSE_BYTES },
);
if (!Array.isArray(rows)) throw malformed();
const { outline, skipped } = selectOutlineResult(rows, {
const { outlines, skipped } = selectOutlineResult(rows, {
kind,
query: sent,
});
const payload = outline
? { status: 'OK', outline }
: { status: 'ZERO_RESULTS', outline: null, skipped };
const found = outlines.length
? { status: 'OK', outlines }
: { status: 'ZERO_RESULTS', outlines: [], skipped };
await cache.set(
cacheKey,
payload,
outline
found,
outlines.length
? NOMINATIM_CACHE_TTL_MS.outline
: NOMINATIM_CACHE_TTL_MS.notFound,
);
return payload;
return found;
},
);
return answer(payload, anchorLat, anchorLon, false);
};
}
@@ -428,6 +444,8 @@ export function geocodeProxy({
kind,
lat: url.searchParams.get('lat'),
lon: url.searchParams.get('lon'),
anchorLat: coordinateParam(url.searchParams.get('alat')),
anchorLon: coordinateParam(url.searchParams.get('alon')),
},
{ signal },
);