Merge remote-tracking branch 'origin/main' into perf/aircraft-label-candidates

This commit is contained in:
Sameh Khamis committed 2026-10-24 23:42:23 -07:00
commit 75eaa56c15
4 files changed
+87 -6

No files matched your search

@@ -0,0 +1,3 @@
- Read a hemisphere letter that is separated from its number, so a search for
`40.7128 N 74.0060 W` flies there instead of falling through to the geocoders
(Rudy Celekli, @rudycelekli, #1340).
+6
View File
@@ -773,6 +773,12 @@ transports and cancellation through response parsing. Stop and application
teardown abort pending connections; reconnect requests a new client secret.
Microphone, radio handoff, tool schemas and default model behavior are preserved.
Coordinate search accepts whitespace-separated decimal degrees with prefix or
suffix hemisphere letters, including `40.7128 N 74.0060 W`. Components require
an actual separator and an unambiguous hemisphere assignment; malformed numeric
fragments and scientific notation fall through to geocoders instead of becoming
exact coordinate results.
Geospatial lookups are composed through `src/search`: forward/reverse geocoding,
text/nearby search and routing use configurable providers/endpoints. Annotation,
HUD and voice consumers share the configured service. Existing Google/Photon
+28 -6
View File
@@ -47,15 +47,37 @@ function readComponent(text) {
};
}
/** Split on a comma, a semicolon, or whitespace — never on anything else. */
/**
* Split on a comma, a semicolon, or whitespace — never on anything else.
* A hemisphere letter stays with the number it names, so `40.7 N 74.0 W`
* is two components. Splitting on the space before that letter used to
* leave three pieces and the search declined the coordinate.
*/
function splitComponents(query) {
const text = String(query).trim();
if (!text) return null;
const parts = /[,;]/.test(text)
? text.split(/[,;]/)
: text.split(/\s+(?=[+-]?[.\d]|[NSEW]\s*[+-]?[.\d])/i);
const trimmed = parts.map((part) => part.trim());
return trimmed.length === 2 && trimmed.every(Boolean) ? trimmed : null;
if (/[,;]/.test(text)) {
const trimmed = text.split(/[,;]/).map((part) => part.trim());
return trimmed.length === 2 && trimmed.every(Boolean) ? trimmed : null;
}
const words = text.split(/\s+/);
// Each component has at most a number, a degree sign and one hemisphere.
if (words.length > 6) return null;
// Keep every character: extracting tokens would turn `1e3` or `1.2.3`
// into invented coordinates. A lone letter between numbers can name either
// component; require an unambiguous split rather than guessing its owner.
let result = null;
for (let boundary = words.length - 1; boundary > 0; boundary -= 1) {
const parts = [
words.slice(0, boundary).join(' '),
words.slice(boundary).join(' '),
];
if (parts.every((part) => readComponent(part))) {
if (result) return null;
result = parts;
}
}
return result;
}
/** Present a parsed pair the way a coordinate is normally written. */
+50
View File
@@ -52,6 +52,19 @@ test('hemisphere letters fix the axis, in either order', () => {
label: '40.0000° N, 74.0000° W',
});
assert.deepEqual(parseCoordinateQuery('40° N, 74° W')?.lat, 40);
// Written the way a coordinate is usually pasted, with no comma.
assert.deepEqual(parseCoordinateQuery('40.7128 N 74.0060 W'), {
lat: 40.7128,
lon: -74.006,
label: '40.7128° N, 74.0060° W',
});
assert.deepEqual(parseCoordinateQuery('40.7128° N 74.0060° W')?.lon, -74.006);
assert.deepEqual(parseCoordinateQuery('N 40.7128 W 74.0060'), {
lat: 40.7128,
lon: -74.006,
label: '40.7128° N, 74.0060° W',
});
assert.deepEqual(parseCoordinateQuery('74.0060 W 40.7128 N')?.lat, 40.7128);
// One letter is enough: it fixes its own axis, the other takes what is left.
assert.deepEqual(parseCoordinateQuery('40 N, -74')?.lon, -74);
assert.deepEqual(parseCoordinateQuery('-74, 40 N'), {
@@ -66,6 +79,21 @@ test('malformed and ambiguous coordinates are refused, never guessed', () => {
// The reviewer's case: trailing text must not be discarded.
'12junk, 34oops',
'12junk,34',
'12junk 34',
'1e3 5',
'1e3',
'1E3',
'1e-3',
'1.2.3',
'12-34',
'40 NW 74',
'40 N 74',
'40 W 74',
'40 E 74',
'°40 74',
'40°° 74',
'40 74°°',
'40 26 46 N 79 58 56 W',
'43.1731, -79.0384x',
'abc, def',
'Paris',
@@ -196,3 +224,25 @@ test('malformed or unsupported geo URIs are refused without losing their meaning
assert.equal(parseCoordinateQuery(query), null, query);
}
});
test('whitespace boundaries preserve degree signs and hemisphere ownership', () => {
for (const query of ['40 ° 74', '40° 74', '40 N, 74', 'N 40 74']) {
assert.equal(parseCoordinateQuery(query)?.lat, 40, query);
assert.equal(parseCoordinateQuery(query)?.lon, 74, query);
}
for (const query of ['40 ° N 74 ° W', 'N 40 ° W 74 °', '40\tN\n74\tW']) {
assert.equal(parseCoordinateQuery(query)?.lat, 40, query);
assert.equal(parseCoordinateQuery(query)?.lon, -74, query);
}
});
test('a signed plain component leaves a spaced letter with the other component', () => {
assert.deepEqual(
parseCoordinateQuery('-40 N 74'),
parseCoordinateQuery('-40, N 74'),
);
assert.deepEqual(
parseCoordinateQuery('40 N -74'),
parseCoordinateQuery('40 N, -74'),
);
});