fix(voice): bound OpenStreetMap text before it reaches voice results

- strip control and format characters from OSM tag values, keeping the
  zero-width (non-)joiners Persian and Indic names spell with, and cut
  them at 120 code points so a name cannot steer or flood the model
- adopt the nine new geometry test files in format-scope.json; five of
  them change by formatting only
This commit is contained in:
Manjunath committed 2026-11-03 14:33:51 +05:30
1 parent ecaddd838b
commit 2a13a06def
7 files changed
+119 -18

No files matched your search

+28 -5
View File
@@ -17,14 +17,24 @@ function fakeOverlayHost() {
}
const RECORDS = [
{ id: 'node/1', lat: 27.7, lon: 85.3, name: 'Bir Hospital', kind: 'hospital' },
{
id: 'node/1',
lat: 27.7,
lon: 85.3,
name: 'Bir Hospital',
kind: 'hospital',
},
{ id: 'node/2', lat: 27.71, lon: 85.31, name: null, kind: 'hospital' },
];
test('results are queryable only while the layer is on, and labels follow names', () => {
const host = fakeOverlayHost();
const layer = createOsmPlacesLayer({ overlayHost: host });
layer.setResults(RECORDS, { kind: 'hospital', label: 'hospitals', area: 'Kathmandu' });
layer.setResults(RECORDS, {
kind: 'hospital',
label: 'hospitals',
area: 'Kathmandu',
});
assert.deepEqual(layer.getAnalystRecords(), [], 'off: nothing to count');
assert.equal(layer.getStats().count, 2);
assert.equal(layer.getStats().statusMessage, 'hospitals · Kathmandu');
@@ -32,10 +42,23 @@ test('results are queryable only while the layer is on, and labels follow names'
const records = layer.getAnalystRecords();
assert.equal(records.length, 2);
records[0].name = 'changed';
assert.equal(layer.getAnalystRecords()[0].name, 'Bir Hospital', 'callers get copies');
assert.deepEqual(layer.getResultsMeta(), { kind: 'hospital', label: 'hospitals', area: 'Kathmandu', count: 2 });
assert.equal(
layer.getAnalystRecords()[0].name,
'Bir Hospital',
'callers get copies',
);
assert.deepEqual(layer.getResultsMeta(), {
kind: 'hospital',
label: 'hospitals',
area: 'Kathmandu',
count: 2,
});
layer.disable();
assert.ok(host.calls.some(([name, id]) => name === 'clearSource' && id === 'osm-places'));
assert.ok(
host.calls.some(
([name, id]) => name === 'clearSource' && id === 'osm-places',
),
);
});
test('an empty layer tells the operator how to fill it', () => {
+27 -2
View File
@@ -8,8 +8,33 @@ export const OSM_PLACES_LAYER_ID = 'osm-places';
/** A node and a way/relation of the same name this close are one site mapped twice. */
const SAME_SITE_M = 75;
const text = (value) =>
typeof value === 'string' && value.trim() ? value.trim() : null;
/** Longest tag value kept; OSM names are open text that reaches the model. */
const MAX_TEXT = 120;
/**
* A tag value as plain bounded text: control characters (newlines, tabs)
* become spaces; format characters (bidi overrides, zero-width spaces) are
* dropped, except the zero-width (non-)joiners Persian and Indic names spell
* with; runs of whitespace collapse, and long values are cut with an
* ellipsis.
*/
function text(value) {
if (typeof value !== 'string') return null;
const clean = value
.replace(/\p{Cc}+/gu, ' ')
.replace(/(?![\u200c\u200d])\p{Cf}/gu, '')
.replace(/\s+/g, ' ')
.trim();
if (!clean) return null;
// Cut on code points so an emoji's surrogate pair is never split.
const chars = Array.from(clean);
return chars.length <= MAX_TEXT
? clean
: `${chars
.slice(0, MAX_TEXT - 1)
.join('')
.trimEnd()}…`;
}
function distanceM(a, b) {
const toRad = Math.PI / 180;