address the final review of the voice outline fixes

- A building word ("Burj Khalifa Tower", "Sagrada Familia Church") passes
  the last-word rule on a building-like result; enclosures stay out.
- The anchor chooses only among the best-named outlines, so a worse name
  never wins by containment alone.
- An ask without a quadrant takes only the quadrant of the matching street
  nearest the point; a leading quadrant ("SE Main St") may go unsaid too.
- On an unbuffered tile edge a fragment shows as an edge along the
  boundary, so a whole building touching it at a corner is not cut.
- Drop two orphaned comments and correct the place-name key wording.
This commit is contained in:
Sameh Khamis committed 2026-11-02 09:03:36 -08:00
1 parent a61d0f4c2a
commit 0712db9891
8 files changed
+194 -42

No files matched your search

+49 -12
View File
@@ -234,8 +234,6 @@ function adminLevelFits(kind, row) {
/** Largest footprint (m²) a non-`building` result may have for a building ask. */
const BUILDING_MAX_AREA_M2 = 150_000;
/** Approximate area (m²) of a [lon, lat] ring. */
/** Whether a result's class/type suits the ask kind. */
export function acceptsOutlineClass(kind, osmClass, osmType) {
const rules = OUTLINE_ACCEPT[kind];
@@ -259,6 +257,36 @@ function queryWords(query) {
.filter((word) => word && !STOP_WORDS.has(word));
}
/**
* Words naming what a building is, which OSM rarely carries in the name or
* type ("Burj Khalifa", building=yes). Enclosures (zoo, garden, arch,
* conservatory) are left out: those name a feature inside a park.
*/
const BUILDING_WORDS = [
'building',
'tower',
'church',
'cathedral',
'basilica',
'chapel',
'temple',
'mosque',
'synagogue',
'shrine',
'museum',
'palace',
'castle',
'hall',
'library',
];
const BUILDING_LIKE_CLASSES = new Set([
'building',
'amenity',
'man_made',
'tourism',
'historic',
]);
/** How well a result's names answer the ask. */
const NAME_MISS = 0;
const NAME_OVERLAP = 1;
@@ -293,7 +321,8 @@ function outlineNameMatch(row, query, kind) {
const unspaced = UNSPACED_SCRIPT_RE.test(whole);
// The last word usually names the feature ("Zoo", "Arch"): a name without
// it is the place around the feature ("Central Park"), unless the word is
// where the result is ("Space Needle Seattle") or what it is ("… Bridge").
// where the result is ("Space Needle Seattle"), what it is ("… Bridge"), or
// a building word on a building-like result ("Burj Khalifa Tower").
const context = new Set(
normalizePlaceName(
[
@@ -308,6 +337,8 @@ function outlineNameMatch(row, query, kind) {
.replace(/_/g, ' '),
).split(' '),
);
if (BUILDING_LIKE_CLASSES.has(String(row?.category || row?.class)))
for (const word of BUILDING_WORDS) context.add(word);
let match = NAME_MISS;
for (const name of names) {
if (
@@ -356,8 +387,6 @@ function cleanRing(ring) {
return out;
}
/** Planar ring area in squared degrees (only used to rank parts). */
/** Polygon/MultiPolygon GeoJSON → `[[outer, ...holes], …]`, largest first. */
function polygonsFromGeoJson(geometry) {
const raw =
@@ -565,21 +594,29 @@ export function selectOutlineResult(rows, { kind, query }) {
});
}
// Stable: equal matches keep upstream order.
const outlines = found
.sort((a, b) => b.nameMatch - a.nameMatch)
.map((entry) => entry.outline);
return { outline: outlines[0] || null, outlines, skipped };
found.sort((a, b) => b.nameMatch - a.nameMatch);
const outlines = found.map((entry) => entry.outline);
const ranks = found.map((entry) => entry.nameMatch);
return { outline: outlines[0] || null, outlines, ranks, skipped };
}
/**
* The outline holding `lat, lon` (the resolved anchor), else the first.
* Among the best-named outlines (`ranks` from selectOutlineResult; all of
* them without), the one holding `lat, lon` (the resolved anchor), else the
* first. A worse name never wins by containment alone: an exact building
* that misses a pin by metres is not replaced by the plaza around it.
* Without an anchor the first answers, as before.
*/
export function outlineAt(outlines, lat, lon) {
export function outlineAt(outlines, lat, lon, ranks = null) {
const list = Array.isArray(outlines) ? outlines : [];
if (!finiteLat(lat) || !finiteLon(lon)) return list[0] || null;
const best = Array.isArray(ranks) ? ranks[0] : undefined;
return (
list.find((outline) => containsAnchor(outline.polygons, lat, lon)) ||
list.find(
(outline, index) =>
(best === undefined || ranks[index] === best) &&
containsAnchor(outline.polygons, lat, lon),
) ||
list[0] ||
null
);
+3 -3
View File
@@ -242,7 +242,7 @@ export function createNominatimOutlineProvider({
const answer = (payload, anchorLat, anchorLon, cached) => ({
status: payload.status,
outline: outlineAt(payload.outlines, anchorLat, anchorLon),
outline: outlineAt(payload.outlines, anchorLat, anchorLon, payload.ranks),
...(payload.skipped ? { skipped: payload.skipped } : {}),
...(cached ? { cached: true } : {}),
});
@@ -277,12 +277,12 @@ export function createNominatimOutlineProvider({
{ signal: sharedSignal, maxBytes: OUTLINE_MAX_RESPONSE_BYTES },
);
if (!Array.isArray(rows)) throw malformed();
const { outlines, skipped } = selectOutlineResult(rows, {
const { outlines, ranks, skipped } = selectOutlineResult(rows, {
kind,
query: sent,
});
const found = outlines.length
? { status: 'OK', outlines }
? { status: 'OK', outlines, ranks }
: { status: 'ZERO_RESULTS', outlines: [], skipped };
await cache.set(
cacheKey,