mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-08 20:34:02 +00:00
fix(geocode): preserve non-Latin names in keyless search
This commit is contained in:
1 parent
7d8b80db0a
commit
e4fda9d4e6
7 files changed
+166
-12
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
- Keep keyless place search on a nearby exact match for names written in
|
||||||
|
non-Latin scripts instead of discarding the match and retrying without its
|
||||||
|
proximity ranking (Mert Kaya, @mertkayacs, #PRNUM).
|
||||||
@@ -5720,8 +5720,10 @@ HTTP refusals and outages remain retryable. Searches share a 12-second total
|
|||||||
deadline, with Photon requests capped at six seconds each. Caller/application
|
deadline, with Photon requests capped at six seconds each. Caller/application
|
||||||
cancellation stops retries and late cache writes. Replacing a location search
|
cancellation stops retries and late cache writes. Replacing a location search
|
||||||
cancels the previous lookup; disposing its controls cancels the active lookup. Photon uses a soft proximity bias, up to five
|
cancels the previous lookup; disposing its controls cancels the active lookup. Photon uses a soft proximity bias, up to five
|
||||||
candidates, and an unbiased retry for name mismatches. A "place, region" query
|
candidates, and an unbiased retry for name mismatches. Name comparison folds
|
||||||
prefers a candidate named exactly for the place whose city, county, state,
|
Latin accents while preserving letters and marks in other scripts, so an exact
|
||||||
|
non-Latin name keeps its proximity ranking. A "place, region" query prefers a
|
||||||
|
candidate named exactly for the place whose city, county, state,
|
||||||
country or country code (or USA/UK) contains every word of the region, and a
|
country or country code (or USA/UK) contains every word of the region, and a
|
||||||
settlement or administrative area over a POI when both fit. Nearby results that
|
settlement or administrative area over a POI when both fit. Nearby results that
|
||||||
fail that check are retried unbiased. After the region checks of both passes, a
|
fail that check are retried unbiased. After the region checks of both passes, a
|
||||||
|
|||||||
@@ -552,6 +552,7 @@
|
|||||||
"src/search/placeSearch.js",
|
"src/search/placeSearch.js",
|
||||||
"src/search/google.js",
|
"src/search/google.js",
|
||||||
"src/keylessGeocoder.js",
|
"src/keylessGeocoder.js",
|
||||||
|
"src/data/placeNames.js",
|
||||||
"src/search/geospatial.js",
|
"src/search/geospatial.js",
|
||||||
"src/search/http.js",
|
"src/search/http.js",
|
||||||
"src/search/defaults.js",
|
"src/search/defaults.js",
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Test fixtures
|
# Test fixtures
|
||||||
|
|
||||||
|
- `photon-moscow-search.json` - Photon responses for `Москва`, captured
|
||||||
|
2026-11-05 with and without a proximity bias near the Tajikistan namesake.
|
||||||
|
The fixture is trimmed to the fields the keyless geocoder consumes and is
|
||||||
|
used only by offline search tests. Data from OpenStreetMap contributors,
|
||||||
|
ODbL 1.0 (https://www.openstreetmap.org/copyright).
|
||||||
- `tomtom-flow-austin-12-935-1686.pbf` — one real TomTom traffic-flow vector
|
- `tomtom-flow-austin-12-935-1686.pbf` — one real TomTom traffic-flow vector
|
||||||
tile (Mapbox Vector Tile protobuf, layer `"Traffic flow"`), downtown Austin
|
tile (Mapbox Vector Tile protobuf, layer `"Traffic flow"`), downtown Austin
|
||||||
z12 x935 y1686, captured 2026-07-16 from
|
z12 x935 y1686, captured 2026-07-16 from
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"biased": {
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [71.8339931, 38.948412]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"name": "Москва",
|
||||||
|
"country": "Тоҷикистон"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [37.6063916, 55.625578]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"name": "Москва",
|
||||||
|
"country": "Россия"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [37.6174782, 55.7505412]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"name": "Москва",
|
||||||
|
"country": "Россия"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [37.0536908, 55.7098009]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"name": "Москва",
|
||||||
|
"country": "Россия"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [32.163334, 56.917778]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"name": "Москва",
|
||||||
|
"country": "Россия"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unbiased": {
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [37.6063916, 55.625578]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"name": "Москва",
|
||||||
|
"country": "Россия"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [37.6174782, 55.7505412]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"name": "Москва",
|
||||||
|
"country": "Россия"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [37.0536908, 55.7098009]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"name": "Москва",
|
||||||
|
"country": "Россия"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [32.163334, 56.917778]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"name": "Москва",
|
||||||
|
"country": "Россия"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [71.8339931, 38.948412]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"name": "Москва",
|
||||||
|
"country": "Тоҷикистон"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { foldPlaceName } from './data/placeNames.js';
|
||||||
|
|
||||||
/** Photon/OpenStreetMap place-search adapter. */
|
/** Photon/OpenStreetMap place-search adapter. */
|
||||||
const PHOTON_ENDPOINT = 'https://photon.komoot.io/api/';
|
const PHOTON_ENDPOINT = 'https://photon.komoot.io/api/';
|
||||||
|
|
||||||
@@ -233,19 +235,15 @@ export function photonSearchUrl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Place name reduced to comparable form: unaccented, lowercased, punctuation
|
* Place name reduced to comparable form: lowercased, Latin accents removed,
|
||||||
* collapsed to single spaces. "Huế" and "Hue" have to compare equal, because a
|
* other scripts preserved, and punctuation collapsed to single spaces. "Huế"
|
||||||
* user typing either means the same city.
|
* and "Hue" have to compare equal, because a user typing either means the same
|
||||||
|
* city.
|
||||||
* @param {string} text
|
* @param {string} text
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function normalizeToponym(text) {
|
export function normalizeToponym(text) {
|
||||||
return String(text ?? '')
|
return foldPlaceName(text);
|
||||||
.normalize('NFD')
|
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9]+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ import {
|
|||||||
import { geocodeNavigationMode } from './locations.js';
|
import { geocodeNavigationMode } from './locations.js';
|
||||||
|
|
||||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const MOSCOW_RESPONSES = JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(ROOT, 'src/data/fixtures/photon-moscow-search.json'),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
/** Photon's real answer for "Hanoi", trimmed to the fields we consume. */
|
/** Photon's real answer for "Hanoi", trimmed to the fields we consume. */
|
||||||
const HANOI = {
|
const HANOI = {
|
||||||
@@ -223,7 +229,6 @@ const HUE = {
|
|||||||
extent: [107.3763, 16.7739, 108.0518, 16.1276],
|
extent: [107.3763, 16.7739, 108.0518, 16.1276],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Answers biased requests with `nearby`, unbiased ones with `anywhere`. */
|
/** Answers biased requests with `nearby`, unbiased ones with `anywhere`. */
|
||||||
function photonPair(nearby, anywhere) {
|
function photonPair(nearby, anywhere) {
|
||||||
const urls = [];
|
const urls = [];
|
||||||
@@ -249,6 +254,36 @@ test('a biased near-miss is refused, and the unbiased answer is taken instead',
|
|||||||
assert.equal(fetchImpl.urls.length, 2, 'the refused bias costs exactly one extra request');
|
assert.equal(fetchImpl.urls.length, 2, 'the refused bias costs exactly one extra request');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a non-Latin exact match keeps the nearby namesake', async () => {
|
||||||
|
const fetchImpl = photonPair(
|
||||||
|
MOSCOW_RESPONSES.biased.features,
|
||||||
|
MOSCOW_RESPONSES.unbiased.features,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await geocodeKeyless('Москва', {
|
||||||
|
bias: '38.90,71.80|39.00,71.90',
|
||||||
|
fetchImpl,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.label, 'Москва, Тоҷикистон');
|
||||||
|
assert.equal(fetchImpl.urls.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toponym folding preserves non-Latin scripts', () => {
|
||||||
|
const cases = [
|
||||||
|
['Huế', 'hue'],
|
||||||
|
['Москва', 'москва'],
|
||||||
|
['Αθήνα', 'αθήνα'],
|
||||||
|
['القاهرة', 'القاهرة'],
|
||||||
|
['東京', '東京'],
|
||||||
|
['São 東京', 'sao 東京'],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [input, expected] of cases) {
|
||||||
|
assert.equal(normalizeToponym(input), expected, input);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('a biased match that leads with the name is taken without a second request', async () => {
|
test('a biased match that leads with the name is taken without a second request', async () => {
|
||||||
// The other half of the rule, and the reason bias exists at all: "Sixth
|
// The other half of the rule, and the reason bias exists at all: "Sixth
|
||||||
// Street" over Austin must stay Austin's.
|
// Street" over Austin must stay Austin's.
|
||||||
|
|||||||
Reference in new issue
Block a user