fix(geocode): a 'place, region' query lands in the region, not on a namesake

From Denver, 'Paris, Texas' flew to a pub in Budapest at rooftop range. The
camera-biased Photon answer listed the town first, but the pub's name equals
the whole query, and the biased path matched names only. A query with a
region now takes a candidate named for the place whose city, county, state,
country or country code (or USA/UK for US/GB) contains every word of the
region, in both the biased and the unbiased pass; with no such candidate
the existing name rules decide. A query with no region after its comma
keeps the biased name match.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
quanticsoul4772andClaude Opus 5.5 committed 2026-10-25 16:31:08 -07:00
1 parent 5ae22ac333
commit 1a61d02b7d
2 files changed
+110 -1

No files matched your search

+50 -1
View File
@@ -266,6 +266,50 @@ export function selectPhotonFeature(
return null;
}
/** Short country names people type that Photon's fields don't carry. */
const COUNTRY_ALIASES = Object.freeze({ US: 'usa', GB: 'uk' });
/** The region part of a "place, region" query, normalized ('' if none). */
function queryRegion(query) {
const [, ...rest] = String(query ?? '').split(',');
return normalizeToponym(rest.join(' '));
}
/**
* 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.
* @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.
*/
export function selectQualifiedFeature(features, query) {
const [headText] = String(query ?? '').split(',');
const region = queryRegion(query);
const head = normalizeToponym(headText);
if (!head || !region) return null;
const words = region.split(' ');
return (
(Array.isArray(features) ? features : []).find((feature) => {
const properties = feature?.properties ?? {};
const name = normalizeToponym(properties.name);
if (name !== head && !name.startsWith(`${head} `)) return false;
const where = ` ${normalizeToponym(
[
properties.city,
properties.county,
properties.state,
properties.country,
properties.countrycode,
COUNTRY_ALIASES[String(properties.countrycode ?? '').toUpperCase()],
].join(' '),
)} `;
return words.every((word) => where.includes(` ${word} `));
}) ?? null
);
}
/** Evict the oldest memo entries until the cache is within its cap. */
function trimPhotonCache(cache) {
while (cache.size > PHOTON_CACHE_MAX) {
@@ -344,12 +388,16 @@ export async function geocodeKeylessWithOutcome(
// to the wrong continent. So a biased answer is only accepted when it leads
// with the name asked for; otherwise the same query runs unbiased, which is
// what a user searching for somewhere off-screen meant in the first place.
// A query naming a region is only answered nearby by a match inside it.
let feature = null;
let everyAskAnswered = true;
if (bias) {
const biased = await ask(photonSearchUrl(trimmed, { bias, endpoint }));
if (biased === null) everyAskAnswered = false;
else feature = selectPhotonFeature(biased, normalizeToponym(trimmed));
else
feature = queryRegion(trimmed)
? selectQualifiedFeature(biased, trimmed)
: selectPhotonFeature(biased, normalizeToponym(trimmed));
}
signal?.throwIfAborted();
@@ -364,6 +412,7 @@ export async function geocodeKeylessWithOutcome(
// the thing being searched for; the tail only says where to look.
const head = normalizeToponym(trimmed.split(',')[0]);
feature =
selectQualifiedFeature(anywhere, trimmed) ||
selectPhotonFeature(anywhere, head, { allowContains: true }) ||
anywhere[0];
}