mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
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:
1 parent
5ae22ac333
commit
1a61d02b7d
2 files changed
+110
-1
No files matched your search
+50
-1
@@ -266,6 +266,50 @@ export function selectPhotonFeature(
|
|||||||
return null;
|
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. */
|
/** Evict the oldest memo entries until the cache is within its cap. */
|
||||||
function trimPhotonCache(cache) {
|
function trimPhotonCache(cache) {
|
||||||
while (cache.size > PHOTON_CACHE_MAX) {
|
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
|
// 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
|
// 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.
|
// 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 feature = null;
|
||||||
let everyAskAnswered = true;
|
let everyAskAnswered = true;
|
||||||
if (bias) {
|
if (bias) {
|
||||||
const biased = await ask(photonSearchUrl(trimmed, { bias, endpoint }));
|
const biased = await ask(photonSearchUrl(trimmed, { bias, endpoint }));
|
||||||
if (biased === null) everyAskAnswered = false;
|
if (biased === null) everyAskAnswered = false;
|
||||||
else feature = selectPhotonFeature(biased, normalizeToponym(trimmed));
|
else
|
||||||
|
feature = queryRegion(trimmed)
|
||||||
|
? selectQualifiedFeature(biased, trimmed)
|
||||||
|
: selectPhotonFeature(biased, normalizeToponym(trimmed));
|
||||||
}
|
}
|
||||||
|
|
||||||
signal?.throwIfAborted();
|
signal?.throwIfAborted();
|
||||||
@@ -364,6 +412,7 @@ export async function geocodeKeylessWithOutcome(
|
|||||||
// the thing being searched for; the tail only says where to look.
|
// the thing being searched for; the tail only says where to look.
|
||||||
const head = normalizeToponym(trimmed.split(',')[0]);
|
const head = normalizeToponym(trimmed.split(',')[0]);
|
||||||
feature =
|
feature =
|
||||||
|
selectQualifiedFeature(anywhere, trimmed) ||
|
||||||
selectPhotonFeature(anywhere, head, { allowContains: true }) ||
|
selectPhotonFeature(anywhere, head, { allowContains: true }) ||
|
||||||
anywhere[0];
|
anywhere[0];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
photonResultTypes,
|
photonResultTypes,
|
||||||
photonSearchUrl,
|
photonSearchUrl,
|
||||||
selectPhotonFeature,
|
selectPhotonFeature,
|
||||||
|
selectQualifiedFeature,
|
||||||
} from './keylessGeocoder.js';
|
} from './keylessGeocoder.js';
|
||||||
import { geocodeNavigationMode } from './locations.js';
|
import { geocodeNavigationMode } from './locations.js';
|
||||||
|
|
||||||
@@ -288,6 +289,52 @@ test('a "place, region" query is matched on the place, not the region', async ()
|
|||||||
assert.equal(result.label.startsWith('Hoàn Kiếm Lake'), true, `got ${result.label}`);
|
assert.equal(result.label.startsWith('Hoàn Kiếm Lake'), true, `got ${result.label}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Photon's real answers for "Paris, Texas" biased to Denver, then unbiased. */
|
||||||
|
const PARIS_TX = {
|
||||||
|
geometry: { type: 'Point', coordinates: [-95.5555, 33.6609] },
|
||||||
|
properties: { osm_key: 'place', osm_value: 'town', type: 'city', name: 'Paris', state: 'Texas', country: 'United States', countrycode: 'US' },
|
||||||
|
};
|
||||||
|
const PARIS_TEXAS_PUB = {
|
||||||
|
geometry: { type: 'Point', coordinates: [19.0606, 47.4868] },
|
||||||
|
properties: { osm_key: 'amenity', osm_value: 'pub', type: 'house', name: 'Paris, Texas', city: 'Budapest', country: 'Magyarország', countrycode: 'HU' },
|
||||||
|
};
|
||||||
|
const PARIS_TEXAS_SALON = {
|
||||||
|
geometry: { type: 'Point', coordinates: [153.03, -27.47] },
|
||||||
|
properties: { osm_key: 'shop', osm_value: 'hairdresser', type: 'house', name: 'Paris Texas', city: 'Brisbane', state: 'Queensland', country: 'Australia', countrycode: 'AU' },
|
||||||
|
};
|
||||||
|
|
||||||
|
test('"Paris, Texas" is the town in Texas, not a pub of that name in Budapest', async () => {
|
||||||
|
// Measured from Denver against the live service: the biased answer listed the
|
||||||
|
// town first, but the pub's name equals the whole query and won, flying the
|
||||||
|
// camera to Budapest at rooftop range.
|
||||||
|
const fetchImpl = photonPair([PARIS_TX, PARIS_TEXAS_PUB, PARIS_TEXAS_SALON], [PARIS_TX]);
|
||||||
|
|
||||||
|
const result = await geocodeKeyless('Paris, Texas', { bias: '39.70,-104.90|39.80,-104.80', fetchImpl });
|
||||||
|
|
||||||
|
assert.equal(Math.round(result.lat), 34, `got ${result.label}`);
|
||||||
|
assert.equal(fetchImpl.urls.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a region is checked against where a candidate is, not its name', () => {
|
||||||
|
const pubFirst = [PARIS_TEXAS_PUB, PARIS_TEXAS_SALON, PARIS_TX];
|
||||||
|
assert.equal(selectQualifiedFeature(pubFirst, 'Paris, Texas'), PARIS_TX);
|
||||||
|
assert.equal(selectQualifiedFeature(pubFirst, 'Paris, United States'), PARIS_TX);
|
||||||
|
assert.equal(selectQualifiedFeature(pubFirst, 'Paris, US'), PARIS_TX);
|
||||||
|
// No region, or a region nothing lies in, leaves the choice to the name rules.
|
||||||
|
assert.equal(selectQualifiedFeature(pubFirst, 'Paris'), null);
|
||||||
|
assert.equal(selectQualifiedFeature(pubFirst, 'Paris, Ontario'), null);
|
||||||
|
assert.equal(selectQualifiedFeature(undefined, 'Paris, Texas'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a region nearby fits nothing, so the unbiased answer decides', async () => {
|
||||||
|
const fetchImpl = photonPair([PARIS_TEXAS_PUB], [PARIS_TX, PARIS_TEXAS_PUB]);
|
||||||
|
|
||||||
|
const result = await geocodeKeyless('Paris, Texas', { bias: '47.40,19.00|47.50,19.10', fetchImpl });
|
||||||
|
|
||||||
|
assert.equal(Math.round(result.lat), 34, `got ${result.label}`);
|
||||||
|
assert.equal(fetchImpl.urls.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
test('accents and punctuation do not change what a name compares as', () => {
|
test('accents and punctuation do not change what a name compares as', () => {
|
||||||
assert.equal(normalizeToponym('Huế'), 'hue');
|
assert.equal(normalizeToponym('Huế'), 'hue');
|
||||||
assert.equal(normalizeToponym('Hạ Long Bay'), 'ha long bay');
|
assert.equal(normalizeToponym('Hạ Long Bay'), 'ha long bay');
|
||||||
@@ -452,3 +499,16 @@ function readGeocodingConsumer(file) {
|
|||||||
? [file, 'annotations/resolver.js'] : [file];
|
? [file, 'annotations/resolver.js'] : [file];
|
||||||
return files.map((name) => fs.readFileSync(path.join(ROOT, 'src', name), 'utf8')).join('\n');
|
return files.map((name) => fs.readFileSync(path.join(ROOT, 'src', name), 'utf8')).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('"USA" and "UK" name their countries, and a trailing comma keeps the nearby answer', async () => {
|
||||||
|
const pubFirst = [PARIS_TEXAS_PUB, PARIS_TX];
|
||||||
|
assert.equal(selectQualifiedFeature(pubFirst, 'Paris, Texas, USA'), PARIS_TX);
|
||||||
|
// "Paris," names no region: the camera-biased pass still answers.
|
||||||
|
const fetchImpl = photonPair([PARIS_TX], []);
|
||||||
|
const result = await geocodeKeyless('Paris,', {
|
||||||
|
bias: '33.60,-95.60|33.70,-95.50',
|
||||||
|
fetchImpl,
|
||||||
|
});
|
||||||
|
assert.equal(Math.round(result.lat), 34, `got ${result.label}`);
|
||||||
|
assert.equal(fetchImpl.urls.length, 1);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user