mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
fix(search): read hemisphere letters separated from the number (#1340)
Support spaced hemisphere coordinates while rejecting malformed numeric fragments and ambiguous component assignments. Include regression tests and runtime documentation. Contribution by @rudycelekli.
This commit is contained in:
1 parent
5b8e6fbc38
commit
b07352ea6a
4 files changed
+86
-5
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).
|
||||||
@@ -768,6 +768,12 @@ transports and cancellation through response parsing. Stop and application
|
|||||||
teardown abort pending connections; reconnect requests a new client secret.
|
teardown abort pending connections; reconnect requests a new client secret.
|
||||||
Microphone, radio handoff, tool schemas and default model behavior are preserved.
|
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,
|
Geospatial lookups are composed through `src/search`: forward/reverse geocoding,
|
||||||
text/nearby search and routing use configurable providers/endpoints. Annotation,
|
text/nearby search and routing use configurable providers/endpoints. Annotation,
|
||||||
HUD and voice consumers share the configured service. Existing Google/Photon
|
HUD and voice consumers share the configured service. Existing Google/Photon
|
||||||
|
|||||||
@@ -47,16 +47,38 @@ 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) {
|
function splitComponents(query) {
|
||||||
const text = String(query).trim();
|
const text = String(query).trim();
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
const parts = /[,;]/.test(text)
|
if (/[,;]/.test(text)) {
|
||||||
? text.split(/[,;]/)
|
const trimmed = text.split(/[,;]/).map((part) => part.trim());
|
||||||
: text.split(/\s+(?=[+-]?[.\d]|[NSEW]\s*[+-]?[.\d])/i);
|
|
||||||
const trimmed = parts.map((part) => part.trim());
|
|
||||||
return trimmed.length === 2 && trimmed.every(Boolean) ? trimmed : null;
|
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. */
|
/** Present a parsed pair the way a coordinate is normally written. */
|
||||||
export function formatCoordinateLabel(lat, lon) {
|
export function formatCoordinateLabel(lat, lon) {
|
||||||
|
|||||||
@@ -52,6 +52,19 @@ test('hemisphere letters fix the axis, in either order', () => {
|
|||||||
label: '40.0000° N, 74.0000° W',
|
label: '40.0000° N, 74.0000° W',
|
||||||
});
|
});
|
||||||
assert.deepEqual(parseCoordinateQuery('40° N, 74° W')?.lat, 40);
|
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.
|
// 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('40 N, -74')?.lon, -74);
|
||||||
assert.deepEqual(parseCoordinateQuery('-74, 40 N'), {
|
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.
|
// The reviewer's case: trailing text must not be discarded.
|
||||||
'12junk, 34oops',
|
'12junk, 34oops',
|
||||||
'12junk,34',
|
'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',
|
'43.1731, -79.0384x',
|
||||||
'abc, def',
|
'abc, def',
|
||||||
'Paris',
|
'Paris',
|
||||||
@@ -196,3 +224,25 @@ test('malformed or unsupported geo URIs are refused without losing their meaning
|
|||||||
assert.equal(parseCoordinateQuery(query), null, query);
|
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'),
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user