mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
refactor: separate Nominatim transport from search composition
This commit is contained in:
1 parent
2b09352463
commit
a79208181f
6 files changed
+157
-126
No files matched your search
@@ -591,6 +591,9 @@ Replacing that service with a different query language requires operation-level
|
||||
feature/geometry adapters, not just changing its URL. Rendering and geometry
|
||||
selection remain consumers of those results.
|
||||
|
||||
`./sources/nominatim` exports the lower-level JSONv2 client and normalizers for
|
||||
server adapters without importing search composition.
|
||||
|
||||
`./sources/http-body` exports bounded text/JSON readers using web primitives;
|
||||
`./sources/overpass` exports the quoted-string/comment lexer. The lexer alone is
|
||||
not a query validator: spatial bounds, timeouts and other policy remain in the
|
||||
|
||||
+2
-1
@@ -229,6 +229,7 @@
|
||||
"./layers/submarine-cables/source": "./src/layers/submarineCables/bundledSource.js",
|
||||
"./sources/http-body": "./src/sources/httpBody.js",
|
||||
"./sources/overpass": "./src/sources/overpass.js",
|
||||
"./search/nominatim": "./src/search/nominatim.js"
|
||||
"./search/nominatim": "./src/search/nominatim.js",
|
||||
"./sources/nominatim": "./src/sources/nominatim.js"
|
||||
}
|
||||
}
|
||||
@@ -674,5 +674,6 @@
|
||||
"src/search/nominatim.js",
|
||||
"src/search/nominatim.test.mjs",
|
||||
"src/sources/httpBody.js",
|
||||
"src/sources/overpass.js"
|
||||
"src/sources/overpass.js",
|
||||
"src/sources/nominatim.js"
|
||||
]
|
||||
@@ -298,7 +298,8 @@
|
||||
"src/search/nominatim.js",
|
||||
"src/nominatimGeocode.js",
|
||||
"src/data/regionalModel.js",
|
||||
"src/sources/httpBody.js"
|
||||
"src/sources/httpBody.js",
|
||||
"src/sources/nominatim.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -1636,7 +1637,8 @@
|
||||
"src/sources/reference.js",
|
||||
"src/search/nominatim.js",
|
||||
"src/nominatimGeocode.js",
|
||||
"src/sources/httpBody.js"
|
||||
"src/sources/httpBody.js",
|
||||
"src/sources/nominatim.js"
|
||||
],
|
||||
"external": [
|
||||
"@mapbox/vector-tile",
|
||||
@@ -1992,7 +1994,8 @@
|
||||
"src/search/nominatim.js",
|
||||
"src/nominatimGeocode.js",
|
||||
"src/data/regionalModel.js",
|
||||
"src/sources/httpBody.js"
|
||||
"src/sources/httpBody.js",
|
||||
"src/sources/nominatim.js"
|
||||
],
|
||||
"external": [
|
||||
"cesium",
|
||||
@@ -2040,7 +2043,8 @@
|
||||
"src/search/nominatim.js",
|
||||
"src/nominatimGeocode.js",
|
||||
"src/data/regionalModel.js",
|
||||
"src/sources/httpBody.js"
|
||||
"src/sources/httpBody.js",
|
||||
"src/sources/nominatim.js"
|
||||
],
|
||||
"external": [
|
||||
"cesium",
|
||||
@@ -2287,7 +2291,8 @@
|
||||
"src/search/google.js",
|
||||
"src/search/geospatial.js",
|
||||
"src/data/regionalModel.js",
|
||||
"src/sources/httpBody.js"
|
||||
"src/sources/httpBody.js",
|
||||
"src/sources/nominatim.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -2300,5 +2305,16 @@
|
||||
"exports": ["./sources/overpass"],
|
||||
"modules": ["src/sources/overpass.js"],
|
||||
"external": []
|
||||
},
|
||||
"nominatim-source": {
|
||||
"exports": ["./sources/nominatim"],
|
||||
"modules": [
|
||||
"src/sources/nominatim.js",
|
||||
"src/nominatimGeocode.js",
|
||||
"src/search/geospatial.js",
|
||||
"src/data/regionalModel.js",
|
||||
"src/sources/httpBody.js"
|
||||
],
|
||||
"external": []
|
||||
}
|
||||
}
|
||||
+9
-119
@@ -1,124 +1,14 @@
|
||||
import {
|
||||
nominatimToGeocodeResult,
|
||||
nominatimViewboxFromBounds,
|
||||
} from '../nominatimGeocode.js';
|
||||
createNominatimClient,
|
||||
normalizeNominatimResult,
|
||||
normalizeNominatimReverse,
|
||||
} from '../sources/nominatim.js';
|
||||
import { normalizeGooglePlace } from './google.js';
|
||||
import { validCoordinate } from './geospatial.js';
|
||||
import { normalizeRegionalPlace } from '../data/regionalModel.js';
|
||||
import { readResponseJsonCapped } from '../sources/httpBody.js';
|
||||
|
||||
/** Retain the established framing/type rules for JSON and JSONv2 results. */
|
||||
export function normalizeNominatimResult(hit) {
|
||||
return nominatimToGeocodeResult(
|
||||
hit && { ...hit, class: hit.class || hit.category },
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeNominatimReverse(hit) {
|
||||
const result = normalizeNominatimResult(hit);
|
||||
const region = normalizeRegionalPlace(hit);
|
||||
if (!result || !region) return null;
|
||||
const clean = (value) =>
|
||||
String(value || '')
|
||||
.replace(/[\x00-\x1f\x7f]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 120);
|
||||
const street = clean(hit.address?.road || hit.address?.pedestrian);
|
||||
return {
|
||||
formattedAddress: result.formatted_address,
|
||||
locality: region.locality,
|
||||
region: region.region,
|
||||
country: region.country,
|
||||
types: result.types,
|
||||
labels: [clean(result.formatted_address)].filter(Boolean),
|
||||
streetLabels: street ? [street] : [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Nominatim JSONv2 transport for explicitly configured endpoints. Callers own
|
||||
* scheduling and request headers; this does not select a public instance.
|
||||
*/
|
||||
export function createNominatimClient({
|
||||
searchEndpoint,
|
||||
reverseEndpoint,
|
||||
fetchImpl = (...args) => fetch(...args),
|
||||
} = {}) {
|
||||
for (const endpoint of [searchEndpoint, reverseEndpoint]) {
|
||||
if (endpoint == null) continue;
|
||||
if (typeof endpoint !== 'string' || !endpoint || /[?#]/.test(endpoint))
|
||||
throw new TypeError('A query-free Nominatim endpoint is required');
|
||||
const url = new URL(endpoint, 'http://localhost');
|
||||
if (
|
||||
!['http:', 'https:'].includes(url.protocol) ||
|
||||
url.username ||
|
||||
url.password ||
|
||||
!(
|
||||
(endpoint.startsWith('/') && !endpoint.startsWith('//')) ||
|
||||
/^https?:\/\//.test(endpoint)
|
||||
)
|
||||
)
|
||||
throw new TypeError('Invalid Nominatim endpoint');
|
||||
}
|
||||
async function request(endpoint, params, signal) {
|
||||
signal?.throwIfAborted();
|
||||
const response = await fetchImpl(
|
||||
`${endpoint}?${new URLSearchParams({
|
||||
format: 'jsonv2',
|
||||
addressdetails: '1',
|
||||
...params,
|
||||
})}`,
|
||||
{ signal, redirect: 'error' },
|
||||
);
|
||||
signal?.throwIfAborted();
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel?.().catch(() => {});
|
||||
throw new Error('Geocoding service unavailable');
|
||||
}
|
||||
return readResponseJsonCapped(response, 512 * 1024, signal);
|
||||
}
|
||||
return {
|
||||
...(searchEndpoint
|
||||
? {
|
||||
async search(query, { bias, signal } = {}) {
|
||||
const viewbox = nominatimViewboxFromBounds(bias);
|
||||
const rows = await request(
|
||||
searchEndpoint,
|
||||
{
|
||||
q: query,
|
||||
limit: '1',
|
||||
...(viewbox ? { viewbox, bounded: '0' } : {}),
|
||||
},
|
||||
signal,
|
||||
);
|
||||
if (!Array.isArray(rows))
|
||||
throw new Error('Invalid geocoding response');
|
||||
if (rows.length && !normalizeNominatimResult(rows[0]))
|
||||
throw new Error('Invalid geocoding result');
|
||||
return rows;
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(reverseEndpoint
|
||||
? {
|
||||
async reverse(latitude, longitude, { signal } = {}) {
|
||||
if (!validCoordinate([longitude, latitude])) return null;
|
||||
const row = await request(
|
||||
reverseEndpoint,
|
||||
{ lat: latitude, lon: longitude },
|
||||
signal,
|
||||
);
|
||||
// A successful, explicit no-result response is distinct from malformed data.
|
||||
if (row?.error === 'Unable to geocode') return null;
|
||||
if (!normalizeNominatimReverse(row))
|
||||
throw new Error('Invalid reverse geocoding result');
|
||||
return row;
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
export {
|
||||
createNominatimClient,
|
||||
normalizeNominatimResult,
|
||||
normalizeNominatimReverse,
|
||||
};
|
||||
|
||||
/** Forward and reverse operations only; feature queries and routing are separate. */
|
||||
export function createNominatimProvider(options = {}) {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
nominatimToGeocodeResult,
|
||||
nominatimViewboxFromBounds,
|
||||
} from '../nominatimGeocode.js';
|
||||
import { validCoordinate } from '../search/geospatial.js';
|
||||
import { normalizeRegionalPlace } from '../data/regionalModel.js';
|
||||
import { readResponseJsonCapped } from './httpBody.js';
|
||||
|
||||
/** Retain the established framing/type rules for JSON and JSONv2 results. */
|
||||
export function normalizeNominatimResult(hit) {
|
||||
return nominatimToGeocodeResult(
|
||||
hit && { ...hit, class: hit.class || hit.category },
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeNominatimReverse(hit) {
|
||||
const result = normalizeNominatimResult(hit);
|
||||
const region = normalizeRegionalPlace(hit);
|
||||
if (!result || !region) return null;
|
||||
const clean = (value) =>
|
||||
String(value || '')
|
||||
.replace(/[\x00-\x1f\x7f]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 120);
|
||||
const street = clean(hit.address?.road || hit.address?.pedestrian);
|
||||
return {
|
||||
formattedAddress: result.formatted_address,
|
||||
locality: region.locality,
|
||||
region: region.region,
|
||||
country: region.country,
|
||||
types: result.types,
|
||||
labels: [clean(result.formatted_address)].filter(Boolean),
|
||||
streetLabels: street ? [street] : [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Nominatim JSONv2 transport for explicitly configured endpoints. Callers own
|
||||
* scheduling and request headers; this does not select a public instance.
|
||||
*/
|
||||
export function createNominatimClient({
|
||||
searchEndpoint,
|
||||
reverseEndpoint,
|
||||
fetchImpl = (...args) => fetch(...args),
|
||||
} = {}) {
|
||||
for (const endpoint of [searchEndpoint, reverseEndpoint]) {
|
||||
if (endpoint == null) continue;
|
||||
if (typeof endpoint !== 'string' || !endpoint || /[?#]/.test(endpoint))
|
||||
throw new TypeError('A query-free Nominatim endpoint is required');
|
||||
const url = new URL(endpoint, 'http://localhost');
|
||||
if (
|
||||
!['http:', 'https:'].includes(url.protocol) ||
|
||||
url.username ||
|
||||
url.password ||
|
||||
!(
|
||||
(endpoint.startsWith('/') && !endpoint.startsWith('//')) ||
|
||||
/^https?:\/\//.test(endpoint)
|
||||
)
|
||||
)
|
||||
throw new TypeError('Invalid Nominatim endpoint');
|
||||
}
|
||||
async function request(endpoint, params, signal) {
|
||||
signal?.throwIfAborted();
|
||||
const response = await fetchImpl(
|
||||
`${endpoint}?${new URLSearchParams({
|
||||
format: 'jsonv2',
|
||||
addressdetails: '1',
|
||||
...params,
|
||||
})}`,
|
||||
{ signal, redirect: 'error' },
|
||||
);
|
||||
signal?.throwIfAborted();
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel?.().catch(() => {});
|
||||
throw new Error('Geocoding service unavailable');
|
||||
}
|
||||
return readResponseJsonCapped(response, 512 * 1024, signal);
|
||||
}
|
||||
return {
|
||||
...(searchEndpoint
|
||||
? {
|
||||
async search(query, { bias, signal } = {}) {
|
||||
const viewbox = nominatimViewboxFromBounds(bias);
|
||||
const rows = await request(
|
||||
searchEndpoint,
|
||||
{
|
||||
q: query,
|
||||
limit: '1',
|
||||
...(viewbox ? { viewbox, bounded: '0' } : {}),
|
||||
},
|
||||
signal,
|
||||
);
|
||||
if (!Array.isArray(rows))
|
||||
throw new Error('Invalid geocoding response');
|
||||
if (rows.length && !normalizeNominatimResult(rows[0]))
|
||||
throw new Error('Invalid geocoding result');
|
||||
return rows;
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(reverseEndpoint
|
||||
? {
|
||||
async reverse(latitude, longitude, { signal } = {}) {
|
||||
if (!validCoordinate([longitude, latitude])) return null;
|
||||
const row = await request(
|
||||
reverseEndpoint,
|
||||
{ lat: latitude, lon: longitude },
|
||||
signal,
|
||||
);
|
||||
// A successful, explicit no-result response is distinct from malformed data.
|
||||
if (row?.error === 'Unable to geocode') return null;
|
||||
if (!normalizeNominatimReverse(row))
|
||||
throw new Error('Invalid reverse geocoding result');
|
||||
return row;
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user