mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +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
|
feature/geometry adapters, not just changing its URL. Rendering and geometry
|
||||||
selection remain consumers of those results.
|
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/http-body` exports bounded text/JSON readers using web primitives;
|
||||||
`./sources/overpass` exports the quoted-string/comment lexer. The lexer alone is
|
`./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
|
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",
|
"./layers/submarine-cables/source": "./src/layers/submarineCables/bundledSource.js",
|
||||||
"./sources/http-body": "./src/sources/httpBody.js",
|
"./sources/http-body": "./src/sources/httpBody.js",
|
||||||
"./sources/overpass": "./src/sources/overpass.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.js",
|
||||||
"src/search/nominatim.test.mjs",
|
"src/search/nominatim.test.mjs",
|
||||||
"src/sources/httpBody.js",
|
"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/search/nominatim.js",
|
||||||
"src/nominatimGeocode.js",
|
"src/nominatimGeocode.js",
|
||||||
"src/data/regionalModel.js",
|
"src/data/regionalModel.js",
|
||||||
"src/sources/httpBody.js"
|
"src/sources/httpBody.js",
|
||||||
|
"src/sources/nominatim.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
@@ -1636,7 +1637,8 @@
|
|||||||
"src/sources/reference.js",
|
"src/sources/reference.js",
|
||||||
"src/search/nominatim.js",
|
"src/search/nominatim.js",
|
||||||
"src/nominatimGeocode.js",
|
"src/nominatimGeocode.js",
|
||||||
"src/sources/httpBody.js"
|
"src/sources/httpBody.js",
|
||||||
|
"src/sources/nominatim.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"@mapbox/vector-tile",
|
"@mapbox/vector-tile",
|
||||||
@@ -1992,7 +1994,8 @@
|
|||||||
"src/search/nominatim.js",
|
"src/search/nominatim.js",
|
||||||
"src/nominatimGeocode.js",
|
"src/nominatimGeocode.js",
|
||||||
"src/data/regionalModel.js",
|
"src/data/regionalModel.js",
|
||||||
"src/sources/httpBody.js"
|
"src/sources/httpBody.js",
|
||||||
|
"src/sources/nominatim.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"cesium",
|
"cesium",
|
||||||
@@ -2040,7 +2043,8 @@
|
|||||||
"src/search/nominatim.js",
|
"src/search/nominatim.js",
|
||||||
"src/nominatimGeocode.js",
|
"src/nominatimGeocode.js",
|
||||||
"src/data/regionalModel.js",
|
"src/data/regionalModel.js",
|
||||||
"src/sources/httpBody.js"
|
"src/sources/httpBody.js",
|
||||||
|
"src/sources/nominatim.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"cesium",
|
"cesium",
|
||||||
@@ -2287,7 +2291,8 @@
|
|||||||
"src/search/google.js",
|
"src/search/google.js",
|
||||||
"src/search/geospatial.js",
|
"src/search/geospatial.js",
|
||||||
"src/data/regionalModel.js",
|
"src/data/regionalModel.js",
|
||||||
"src/sources/httpBody.js"
|
"src/sources/httpBody.js",
|
||||||
|
"src/sources/nominatim.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
@@ -2300,5 +2305,16 @@
|
|||||||
"exports": ["./sources/overpass"],
|
"exports": ["./sources/overpass"],
|
||||||
"modules": ["src/sources/overpass.js"],
|
"modules": ["src/sources/overpass.js"],
|
||||||
"external": []
|
"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 {
|
import {
|
||||||
nominatimToGeocodeResult,
|
createNominatimClient,
|
||||||
nominatimViewboxFromBounds,
|
normalizeNominatimResult,
|
||||||
} from '../nominatimGeocode.js';
|
normalizeNominatimReverse,
|
||||||
|
} from '../sources/nominatim.js';
|
||||||
import { normalizeGooglePlace } from './google.js';
|
import { normalizeGooglePlace } from './google.js';
|
||||||
import { validCoordinate } from './geospatial.js';
|
export {
|
||||||
import { normalizeRegionalPlace } from '../data/regionalModel.js';
|
createNominatimClient,
|
||||||
import { readResponseJsonCapped } from '../sources/httpBody.js';
|
normalizeNominatimResult,
|
||||||
|
normalizeNominatimReverse,
|
||||||
/** 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;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Forward and reverse operations only; feature queries and routing are separate. */
|
/** Forward and reverse operations only; feature queries and routing are separate. */
|
||||||
export function createNominatimProvider(options = {}) {
|
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