fix(geocode): preserve non-Latin names in keyless search

This commit is contained in:
Mert Kaya committed 2026-11-05 19:09:00 +03:00
1 parent 7d8b80db0a
commit e4fda9d4e6
7 files changed
+166 -12

No files matched your search

+5
View File
@@ -1,5 +1,10 @@
# 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
tile (Mapbox Vector Tile protobuf, layer `"Traffic flow"`), downtown Austin
z12 x935 y1686, captured 2026-07-16 from
+110
View File
@@ -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": "Тоҷикистон"
}
}
]
}
}
+7 -9
View File
@@ -1,3 +1,5 @@
import { foldPlaceName } from './data/placeNames.js';
/** Photon/OpenStreetMap place-search adapter. */
const PHOTON_ENDPOINT = 'https://photon.komoot.io/api/';
@@ -233,19 +235,15 @@ export function photonSearchUrl(
}
/**
* Place name reduced to comparable form: unaccented, lowercased, punctuation
* collapsed to single spaces. "Huế" and "Hue" have to compare equal, because a
* user typing either means the same city.
* Place name reduced to comparable form: lowercased, Latin accents removed,
* other scripts preserved, and punctuation collapsed to single spaces. "Huế"
* and "Hue" have to compare equal, because a user typing either means the same
* city.
* @param {string} text
* @returns {string}
*/
export function normalizeToponym(text) {
return String(text ?? '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, ' ')
.trim();
return foldPlaceName(text);
}
/**
+36 -1
View File
@@ -35,6 +35,12 @@ import {
import { geocodeNavigationMode } from './locations.js';
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. */
const HANOI = {
@@ -223,7 +229,6 @@ const HUE = {
extent: [107.3763, 16.7739, 108.0518, 16.1276],
},
};
/** Answers biased requests with `nearby`, unbiased ones with `anywhere`. */
function photonPair(nearby, anywhere) {
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');
});
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 () => {
// The other half of the rule, and the reason bias exists at all: "Sixth
// Street" over Austin must stay Austin's.