fix(geocode): prefer the settlement when a POI also fits the region

When several candidates fit a "place, region" query, take a settlement or
administrative area over a POI of the same name, so a cafe called "Paris"
in Lyon cannot win "Paris, France" from the nearby pass.

Tests: region queries without a camera bias, whole-word and every-word
region matching, city and county regions, the nearby whole-name answer when
the unbiased request fails, and the settlement preference. The new tests sit
with the other Paris, Texas tests. The station fixture uses its real city.
CURRENT-STATE now describes the order the code applies.
This commit is contained in:
Said Turksever committed 2026-10-28 14:09:56 +00:00
1 parent 81680c5a15
commit 7788168d73
3 files changed
+218 -121

No files matched your search

+19 -4
View File
@@ -284,6 +284,13 @@ export function selectPhotonFeature(
/** Short country names people type that Photon's fields don't carry. */
const COUNTRY_ALIASES = Object.freeze({ US: 'usa', GB: 'uk' });
/**
* OSM keys of settlements and administrative areas. Among candidates that fit
* a "place, region" query, these are what the region names a place in: a cafe
* called "Paris" in Lyon is in France too, but "Paris, France" means the city.
*/
const SETTLEMENT_OSM_KEYS = Object.freeze(new Set(['place', 'boundary']));
/** The region part of a "place, region" query, normalized ('' if none). */
function queryRegion(query) {
const [, ...rest] = String(query ?? '').split(',');
@@ -294,7 +301,8 @@ function queryRegion(query) {
* For a "place, region" query, pick the candidate named for the place that
* actually lies in the region. "Paris, Texas" is also the name of a pub in
* Budapest: matched as one name it beat the town, so the region has to be
* checked against where each candidate is, not against its name.
* checked against where each candidate is, not against its name. When several
* candidates fit, a settlement or administrative area wins over a POI.
* @param {object[]} features - Photon features, in the order returned.
* @param {string} query - The query as typed.
* @returns {?object} null when the query names no region or nothing fits.
@@ -305,8 +313,8 @@ export function selectQualifiedFeature(features, query) {
const head = normalizeToponym(headText);
if (!head || !region) return null;
const words = region.split(' ');
return (
(Array.isArray(features) ? features : []).find((feature) => {
const qualified = (Array.isArray(features) ? features : []).filter(
(feature) => {
const properties = feature?.properties ?? {};
const name = normalizeToponym(properties.name);
// Exactly the place: "Paris Gare de Lyon" is in France too, but it
@@ -323,7 +331,14 @@ export function selectQualifiedFeature(features, query) {
].join(' '),
)} `;
return words.every((word) => where.includes(` ${word} `));
}) ?? null
},
);
return (
qualified.find((feature) =>
SETTLEMENT_OSM_KEYS.has(feature?.properties?.osm_key),
) ??
qualified[0] ??
null
);
}