mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
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:
1 parent
81680c5a15
commit
7788168d73
3 files changed
+218
-121
No files matched your search
+193
-112
@@ -335,6 +335,199 @@ test('a region nearby fits nothing, so the unbiased answer decides', async () =>
|
||||
assert.equal(fetchImpl.urls.length, 2);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
test('a region match takes the place itself, not a place whose name starts with it', () => {
|
||||
const station = {
|
||||
geometry: { type: 'Point', coordinates: [4.86, 45.76] },
|
||||
properties: {
|
||||
osm_key: 'railway',
|
||||
name: 'Paris Gare de Lyon',
|
||||
city: 'Paris',
|
||||
country: 'France',
|
||||
countrycode: 'FR',
|
||||
},
|
||||
};
|
||||
const paris = {
|
||||
geometry: { type: 'Point', coordinates: [2.35, 48.86] },
|
||||
properties: {
|
||||
osm_key: 'place',
|
||||
name: 'Paris',
|
||||
state: 'Île-de-France',
|
||||
country: 'France',
|
||||
countrycode: 'FR',
|
||||
},
|
||||
};
|
||||
assert.equal(selectQualifiedFeature([station, paris], 'Paris, France'), paris);
|
||||
assert.equal(selectQualifiedFeature([station], 'Paris, France'), null);
|
||||
});
|
||||
|
||||
test('"UK" names the United Kingdom', () => {
|
||||
const london = {
|
||||
geometry: { type: 'Point', coordinates: [-0.13, 51.51] },
|
||||
properties: {
|
||||
osm_key: 'place',
|
||||
name: 'London',
|
||||
country: 'United Kingdom',
|
||||
countrycode: 'GB',
|
||||
},
|
||||
};
|
||||
const ontario = {
|
||||
geometry: { type: 'Point', coordinates: [-81.25, 42.98] },
|
||||
properties: {
|
||||
osm_key: 'place',
|
||||
name: 'London',
|
||||
state: 'Ontario',
|
||||
country: 'Canada',
|
||||
countrycode: 'CA',
|
||||
},
|
||||
};
|
||||
assert.equal(selectQualifiedFeature([ontario, london], 'London, UK'), london);
|
||||
});
|
||||
|
||||
test('a nearby place whose own name has a comma still wins when nothing matches as place and region', async () => {
|
||||
const store = (lon, lat, city) => ({
|
||||
geometry: { type: 'Point', coordinates: [lon, lat] },
|
||||
properties: {
|
||||
osm_key: 'shop',
|
||||
osm_value: 'houseware',
|
||||
name: 'Bed, Bath & Beyond',
|
||||
city,
|
||||
countrycode: 'US',
|
||||
},
|
||||
});
|
||||
const nearby = store(-104.94, 39.66, 'Denver');
|
||||
const fetchImpl = photonPair([nearby], [store(-73.99, 40.74, 'New York')]);
|
||||
|
||||
const result = await geocodeKeyless('Bed, Bath & Beyond', {
|
||||
bias: '39.60,-105.00|39.70,-104.90',
|
||||
fetchImpl,
|
||||
});
|
||||
|
||||
assert.match(result.label, /Denver/);
|
||||
// Main answered this from the nearby pass alone. The second request is the
|
||||
// accepted cost of checking the region unbiased first.
|
||||
assert.equal(fetchImpl.urls.length, 2, 'the region checks ran first');
|
||||
});
|
||||
|
||||
test('the nearby whole-name fallback takes an exact name, not one that only starts with the query', async () => {
|
||||
const place = (name, lon, lat, props) => ({
|
||||
geometry: { type: 'Point', coordinates: [lon, lat] },
|
||||
properties: { osm_key: 'place', name, ...props },
|
||||
});
|
||||
const nearby = [
|
||||
place('Springfield IL Auto', -89.6, 39.8, {
|
||||
osm_key: 'shop',
|
||||
city: 'Springfield',
|
||||
state: 'Illinois',
|
||||
countrycode: 'US',
|
||||
}),
|
||||
];
|
||||
const anywhere = [
|
||||
place('Springfield', -89.65, 39.78, { state: 'Illinois', countrycode: 'US' }),
|
||||
];
|
||||
const fetchImpl = photonPair(nearby, anywhere);
|
||||
|
||||
const result = await geocodeKeyless('Springfield, IL', {
|
||||
bias: '39.70,-89.70|39.90,-89.50',
|
||||
fetchImpl,
|
||||
});
|
||||
|
||||
assert.doesNotMatch(result.label, /Auto/);
|
||||
assert.match(result.label, /^Springfield/);
|
||||
});
|
||||
|
||||
test('a region query without a camera bias still takes the place in the region', async () => {
|
||||
// Unbiased, Photon can list the pub first; main took it because its name
|
||||
// starts with "Paris".
|
||||
const fetchImpl = photonPair([], [PARIS_TEXAS_PUB, PARIS_TX]);
|
||||
|
||||
const result = await geocodeKeyless('Paris, Texas', { fetchImpl });
|
||||
|
||||
assert.equal(Math.round(result.lat), 34, `got ${result.label}`);
|
||||
assert.equal(fetchImpl.urls.length, 1);
|
||||
});
|
||||
|
||||
test('a region is matched by whole words, and every word has to fit', () => {
|
||||
const londonUk = {
|
||||
geometry: { type: 'Point', coordinates: [-0.13, 51.51] },
|
||||
properties: { osm_key: 'place', name: 'London', state: 'England', country: 'United Kingdom', countrycode: 'GB' },
|
||||
};
|
||||
// "on" is inside "london" and "england", but it is not a word of either.
|
||||
assert.equal(selectQualifiedFeature([londonUk], 'London, ON'), null);
|
||||
// "Texas" fits, "Ontario" does not, so the town does not qualify.
|
||||
assert.equal(selectQualifiedFeature([PARIS_TX], 'Paris, Texas, Ontario'), null);
|
||||
assert.equal(selectQualifiedFeature([PARIS_TX], 'Paris, Tex'), null);
|
||||
});
|
||||
|
||||
test('a region can be the city or the county a candidate lies in', () => {
|
||||
const street = {
|
||||
geometry: { type: 'Point', coordinates: [-105.27, 40.02] },
|
||||
properties: { osm_key: 'highway', name: 'Main Street', city: 'Boulder', countrycode: 'US' },
|
||||
};
|
||||
const town = {
|
||||
geometry: { type: 'Point', coordinates: [-95.5555, 33.6609] },
|
||||
properties: { osm_key: 'place', name: 'Paris', county: 'Lamar', countrycode: 'US' },
|
||||
};
|
||||
assert.equal(selectQualifiedFeature([street], 'Main Street, Boulder'), street);
|
||||
assert.equal(selectQualifiedFeature([town], 'Paris, Lamar'), town);
|
||||
});
|
||||
|
||||
test('when a POI and the place both fit the region, the place wins', async () => {
|
||||
// A cafe named "Paris" in Lyon is in France too. Nearby it can rank above the
|
||||
// city, and it would frame at rooftop range.
|
||||
const cafe = {
|
||||
geometry: { type: 'Point', coordinates: [4.84, 45.76] },
|
||||
properties: { osm_key: 'amenity', osm_value: 'cafe', name: 'Paris', city: 'Lyon', country: 'France', countrycode: 'FR' },
|
||||
};
|
||||
const paris = {
|
||||
geometry: { type: 'Point', coordinates: [2.35, 48.86] },
|
||||
properties: { osm_key: 'place', osm_value: 'city', type: 'city', name: 'Paris', state: 'Île-de-France', country: 'France', countrycode: 'FR' },
|
||||
};
|
||||
const fetchImpl = photonPair([cafe, paris], [paris]);
|
||||
|
||||
const result = await geocodeKeyless('Paris, France', { bias: '45.70,4.80|45.80,4.90', fetchImpl });
|
||||
|
||||
assert.equal(Math.round(result.lat), 49, `got ${result.label}`);
|
||||
assert.equal(fetchImpl.urls.length, 1);
|
||||
// A POI alone still qualifies: "Main Street, Denver" names a street.
|
||||
assert.equal(selectQualifiedFeature([cafe], 'Paris, France'), cafe);
|
||||
});
|
||||
|
||||
test('if the unbiased request fails, the nearby whole-name match is still the answer', async () => {
|
||||
const store = {
|
||||
geometry: { type: 'Point', coordinates: [-104.94, 39.66] },
|
||||
properties: { osm_key: 'shop', name: 'Bed, Bath & Beyond', city: 'Denver', countrycode: 'US' },
|
||||
};
|
||||
const urls = [];
|
||||
const fetchImpl = (url) => {
|
||||
urls.push(url);
|
||||
if (!new URL(url).searchParams.has('lat')) return Promise.resolve({ ok: false, status: 503 });
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve({ features: [store] }) });
|
||||
};
|
||||
|
||||
const outcome = await geocodeKeylessWithOutcome('Bed, Bath & Beyond', {
|
||||
bias: '39.60,-105.00|39.70,-104.90',
|
||||
fetchImpl,
|
||||
cache: new Map(),
|
||||
});
|
||||
|
||||
assert.match(outcome.place.label, /Denver/);
|
||||
assert.equal(outcome.answered, true);
|
||||
assert.equal(urls.length, 2);
|
||||
});
|
||||
|
||||
test('accents and punctuation do not change what a name compares as', () => {
|
||||
assert.equal(normalizeToponym('Huế'), 'hue');
|
||||
assert.equal(normalizeToponym('Hạ Long Bay'), 'ha long bay');
|
||||
@@ -500,91 +693,6 @@ function readGeocodingConsumer(file) {
|
||||
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);
|
||||
});
|
||||
|
||||
test('a region match takes the place itself, not a place whose name starts with it', () => {
|
||||
const station = {
|
||||
geometry: { type: 'Point', coordinates: [4.86, 45.76] },
|
||||
properties: {
|
||||
osm_key: 'railway',
|
||||
name: 'Paris Gare de Lyon',
|
||||
city: 'Lyon',
|
||||
country: 'France',
|
||||
countrycode: 'FR',
|
||||
},
|
||||
};
|
||||
const paris = {
|
||||
geometry: { type: 'Point', coordinates: [2.35, 48.86] },
|
||||
properties: {
|
||||
osm_key: 'place',
|
||||
name: 'Paris',
|
||||
state: 'Île-de-France',
|
||||
country: 'France',
|
||||
countrycode: 'FR',
|
||||
},
|
||||
};
|
||||
assert.equal(selectQualifiedFeature([station, paris], 'Paris, France'), paris);
|
||||
assert.equal(selectQualifiedFeature([station], 'Paris, France'), null);
|
||||
});
|
||||
|
||||
test('"UK" names the United Kingdom', () => {
|
||||
const london = {
|
||||
geometry: { type: 'Point', coordinates: [-0.13, 51.51] },
|
||||
properties: {
|
||||
osm_key: 'place',
|
||||
name: 'London',
|
||||
country: 'United Kingdom',
|
||||
countrycode: 'GB',
|
||||
},
|
||||
};
|
||||
const ontario = {
|
||||
geometry: { type: 'Point', coordinates: [-81.25, 42.98] },
|
||||
properties: {
|
||||
osm_key: 'place',
|
||||
name: 'London',
|
||||
state: 'Ontario',
|
||||
country: 'Canada',
|
||||
countrycode: 'CA',
|
||||
},
|
||||
};
|
||||
assert.equal(selectQualifiedFeature([ontario, london], 'London, UK'), london);
|
||||
});
|
||||
|
||||
test('a nearby place whose own name has a comma still wins when nothing matches as place and region', async () => {
|
||||
const store = (lon, lat, city) => ({
|
||||
geometry: { type: 'Point', coordinates: [lon, lat] },
|
||||
properties: {
|
||||
osm_key: 'shop',
|
||||
osm_value: 'houseware',
|
||||
name: 'Bed, Bath & Beyond',
|
||||
city,
|
||||
countrycode: 'US',
|
||||
},
|
||||
});
|
||||
const nearby = store(-104.94, 39.66, 'Denver');
|
||||
const fetchImpl = photonPair([nearby], [store(-73.99, 40.74, 'New York')]);
|
||||
|
||||
const result = await geocodeKeyless('Bed, Bath & Beyond', {
|
||||
bias: '39.60,-105.00|39.70,-104.90',
|
||||
fetchImpl,
|
||||
});
|
||||
|
||||
assert.match(result.label, /Denver/);
|
||||
assert.equal(fetchImpl.urls.length, 2, 'the region checks ran first');
|
||||
});
|
||||
|
||||
|
||||
test('wrapped view bias stays near the antimeridian and malformed coordinates are omitted', () => {
|
||||
for (const [bias, expected] of [['-20,170|-10,-171', 179.5], ['50,175|60,-165', -175], ['0,180|10,-180', 180], ['0,-180|10,180', 0]]) {
|
||||
const params = new URL(photonSearchUrl('island', { bias })).searchParams;
|
||||
@@ -594,30 +702,3 @@ test('wrapped view bias stays near the antimeridian and malformed coordinates ar
|
||||
assert.equal(new URL(photonSearchUrl('island', { bias })).searchParams.has('lon'), false);
|
||||
}
|
||||
});
|
||||
|
||||
test('the nearby whole-name fallback takes an exact name, not one that only starts with the query', async () => {
|
||||
const place = (name, lon, lat, props) => ({
|
||||
geometry: { type: 'Point', coordinates: [lon, lat] },
|
||||
properties: { osm_key: 'place', name, ...props },
|
||||
});
|
||||
const nearby = [
|
||||
place('Springfield IL Auto', -89.6, 39.8, {
|
||||
osm_key: 'shop',
|
||||
city: 'Springfield',
|
||||
state: 'Illinois',
|
||||
countrycode: 'US',
|
||||
}),
|
||||
];
|
||||
const anywhere = [
|
||||
place('Springfield', -89.65, 39.78, { state: 'Illinois', countrycode: 'US' }),
|
||||
];
|
||||
const fetchImpl = photonPair(nearby, anywhere);
|
||||
|
||||
const result = await geocodeKeyless('Springfield, IL', {
|
||||
bias: '39.70,-89.70|39.90,-89.50',
|
||||
fetchImpl,
|
||||
});
|
||||
|
||||
assert.doesNotMatch(result.label, /Auto/);
|
||||
assert.match(result.label, /^Springfield/);
|
||||
});
|
||||
Reference in new issue
Block a user