gadm-ts/dist/osm.js
2026-03-23 17:35:02 +01:00

167 lines
12 KiB
JavaScript

/**
* osm.ts — OpenStreetMap Nominatim integration for neighborhood-level lookups.
*
* Complements GADM (which stops at municipality level) with OSM's
* suburb/quarter/neighbourhood data.
*
* Uses Nominatim API — free, no key, but:
* - Max 1 req/sec (we add a small delay)
* - Must set a proper User-Agent
* - Results cached to disk to avoid repeat calls
*/
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { createHash } from 'crypto';
// ────────── cache helpers ──────────
function cacheKey(prefix, input) {
return `${prefix}_${createHash('md5').update(input).digest('hex')}`;
}
function readCache(dir, key) {
const fp = join(dir, 'osm', `${key}.json`);
if (!existsSync(fp))
return null;
try {
return JSON.parse(readFileSync(fp, 'utf-8'));
}
catch {
return null;
}
}
function writeCache(dir, key, data) {
try {
const fp = join(dir, 'osm', `${key}.json`);
mkdirSync(dirname(fp), { recursive: true });
writeFileSync(fp, JSON.stringify(data, null, 2));
}
catch (e) {
console.error('[OSM] cache write failed:', e);
}
}
// ────────── API ──────────
const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org';
const USER_AGENT = 'PolymechGADM/1.0 (dev-trial)';
/** Rate limit: track last request time */
let lastRequestMs = 0;
async function nominatimFetch(path, params) {
// Respect 1 req/sec rate limit
const now = Date.now();
const elapsed = now - lastRequestMs;
if (elapsed < 1100) {
await new Promise(r => setTimeout(r, 1100 - elapsed));
}
lastRequestMs = Date.now();
const url = new URL(path, NOMINATIM_BASE);
url.searchParams.set('format', 'json');
for (const [k, v] of Object.entries(params)) {
url.searchParams.set(k, v);
}
const res = await fetch(url.toString(), {
headers: { 'User-Agent': USER_AGENT },
});
if (!res.ok) {
throw new Error(`Nominatim ${res.status}: ${res.statusText}`);
}
return res.json();
}
/**
* Search OSM Nominatim for a place.
*
* ```ts
* const results = await osmSearch({ query: 'Johannstadt, Dresden', countryCode: 'de' });
* ```
*/
export async function osmSearch(opts) {
const key = cacheKey('search', `${opts.query}_${opts.countryCode || ''}_${opts.limit || 10}`);
if (opts.cacheDir) {
const cached = readCache(opts.cacheDir, key);
if (cached)
return cached;
}
const params = {
q: opts.query,
limit: String(opts.limit || 10),
addressdetails: '1',
};
if (opts.countryCode) {
params.countrycodes = opts.countryCode;
}
const raw = await nominatimFetch('/search', params);
const results = raw.map((r) => ({
placeId: r.place_id,
displayName: r.display_name,
name: r.name || r.display_name.split(',')[0],
type: r.type,
class: r.class,
lat: parseFloat(r.lat),
lon: parseFloat(r.lon),
address: r.address,
}));
if (opts.cacheDir) {
writeCache(opts.cacheDir, key, results);
}
return results;
}
/**
* Get neighborhoods/suburbs within a city using Overpass API.
* Returns place nodes tagged as suburb, quarter, or neighbourhood.
*
* ```ts
* const hoods = await osmNeighborhoods({ city: 'Dresden', country: 'Germany' });
* ```
*/
export async function osmNeighborhoods(opts) {
const key = cacheKey('neighborhoods', `${opts.city}_${opts.country || ''}`);
if (opts.cacheDir) {
const cached = readCache(opts.cacheDir, key);
if (cached)
return cached;
}
// Use Overpass API to find all suburbs/quarters/neighbourhoods in the city
const cityFilter = opts.country
? `"${opts.city}", "${opts.country}"`
: `"${opts.city}"`;
const overpassQuery = `
[out:json][timeout:30];
area[name=${cityFilter}]->.searchArea;
(
node["place"="suburb"](area.searchArea);
node["place"="quarter"](area.searchArea);
node["place"="neighbourhood"](area.searchArea);
node["place"="city_district"](area.searchArea);
);
out body;
`.trim();
const res = await fetch('https://overpass-api.de/api/interpreter', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': USER_AGENT,
},
body: `data=${encodeURIComponent(overpassQuery)}`,
});
if (!res.ok) {
throw new Error(`Overpass API ${res.status}: ${res.statusText}`);
}
const data = await res.json();
const results = (data.elements || []).map((el) => ({
placeId: el.id,
displayName: `${el.tags?.name || 'Unknown'}, ${opts.city}`,
name: el.tags?.name || 'Unknown',
type: el.tags?.place || 'unknown',
class: 'place',
lat: el.lat,
lon: el.lon,
address: {
suburb: el.tags?.place === 'suburb' ? el.tags?.name : undefined,
city: opts.city,
country: opts.country,
},
}));
// Sort alphabetically
results.sort((a, b) => a.name.localeCompare(b.name));
if (opts.cacheDir) {
writeCache(opts.cacheDir, key, results);
}
return results;
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3NtLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL29zbS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7OztHQVVHO0FBRUgsT0FBTyxFQUFFLFVBQVUsRUFBRSxZQUFZLEVBQUUsYUFBYSxFQUFFLFNBQVMsRUFBRSxNQUFNLElBQUksQ0FBQztBQUN4RSxPQUFPLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBRSxNQUFNLE1BQU0sQ0FBQztBQUNyQyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBb0RwQyxzQ0FBc0M7QUFFdEMsU0FBUyxRQUFRLENBQUMsTUFBYyxFQUFFLEtBQWE7SUFDM0MsT0FBTyxHQUFHLE1BQU0sSUFBSSxVQUFVLENBQUMsS0FBSyxDQUFDLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDO0FBQ3hFLENBQUM7QUFFRCxTQUFTLFNBQVMsQ0FBSSxHQUFXLEVBQUUsR0FBVztJQUMxQyxNQUFNLEVBQUUsR0FBRyxJQUFJLENBQUMsR0FBRyxFQUFFLEtBQUssRUFBRSxHQUFHLEdBQUcsT0FBTyxDQUFDLENBQUM7SUFDM0MsSUFBSSxDQUFDLFVBQVUsQ0FBQyxFQUFFLENBQUM7UUFBRSxPQUFPLElBQUksQ0FBQztJQUNqQyxJQUFJLENBQUM7UUFDRCxPQUFPLElBQUksQ0FBQyxLQUFLLENBQUMsWUFBWSxDQUFDLEVBQUUsRUFBRSxPQUFPLENBQUMsQ0FBTSxDQUFDO0lBQ3RELENBQUM7SUFBQyxNQUFNLENBQUM7UUFDTCxPQUFPLElBQUksQ0FBQztJQUNoQixDQUFDO0FBQ0wsQ0FBQztBQUVELFNBQVMsVUFBVSxDQUFDLEdBQVcsRUFBRSxHQUFXLEVBQUUsSUFBYTtJQUN2RCxJQUFJLENBQUM7UUFDRCxNQUFNLEVBQUUsR0FBRyxJQUFJLENBQUMsR0FBRyxFQUFFLEtBQUssRUFBRSxHQUFHLEdBQUcsT0FBTyxDQUFDLENBQUM7UUFDM0MsU0FBUyxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUMsRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDO1FBQzVDLGFBQWEsQ0FBQyxFQUFFLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDckQsQ0FBQztJQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7UUFDVCxPQUFPLENBQUMsS0FBSyxDQUFDLDJCQUEyQixFQUFFLENBQUMsQ0FBQyxDQUFDO0lBQ2xELENBQUM7QUFDTCxDQUFDO0FBRUQsNEJBQTRCO0FBRTVCLE1BQU0sY0FBYyxHQUFHLHFDQUFxQyxDQUFDO0FBQzdELE1BQU0sVUFBVSxHQUFHLDhCQUE4QixDQUFDO0FBRWxELDBDQUEwQztBQUMxQyxJQUFJLGFBQWEsR0FBRyxDQUFDLENBQUM7QUFFdEIsS0FBSyxVQUFVLGNBQWMsQ0FBQyxJQUFZLEVBQUUsTUFBOEI7SUFDdEUsK0JBQStCO0lBQy9CLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztJQUN2QixNQUFNLE9BQU8sR0FBRyxHQUFHLEdBQUcsYUFBYSxDQUFDO0lBQ3BDLElBQUksT0FBTyxHQUFHLElBQUksRUFBRSxDQUFDO1FBQ2pCLE1BQU0sSUFBSSxPQUFPLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxVQUFVLENBQUMsQ0FBQyxFQUFFLElBQUksR0FBRyxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQzFELENBQUM7SUFDRCxhQUFhLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO0lBRTNCLE1BQU0sR0FBRyxHQUFHLElBQUksR0FBRyxDQUFDLElBQUksRUFBRSxjQUFjLENBQUMsQ0FBQztJQUMxQyxHQUFHLENBQUMsWUFBWSxDQUFDLEdBQUcsQ0FBQyxRQUFRLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDdkMsS0FBSyxNQUFNLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxJQUFJLE1BQU0sQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztRQUMxQyxHQUFHLENBQUMsWUFBWSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUM7SUFDL0IsQ0FBQztJQUVELE1BQU0sR0FBRyxHQUFHLE1BQU0sS0FBSyxDQUFDLEdBQUcsQ0FBQyxRQUFRLEVBQUUsRUFBRTtRQUNwQyxPQUFPLEVBQUUsRUFBRSxZQUFZLEVBQUUsVUFBVSxFQUFFO0tBQ3hDLENBQUMsQ0FBQztJQUVILElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxFQUFFLENBQUM7UUFDVixNQUFNLElBQUksS0FBSyxDQUFDLGFBQWEsR0FBRyxDQUFDLE1BQU0sS0FBSyxHQUFHLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FBQztJQUNsRSxDQUFDO0lBRUQsT0FBTyxHQUFHLENBQUMsSUFBSSxFQUFFLENBQUM7QUFDdEIsQ0FBQztBQUVEOzs7Ozs7R0FNRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsU0FBUyxDQUFDLElBQXNCO0lBQ2xELE1BQU0sR0FBRyxHQUFHLFFBQVEsQ0FBQyxRQUFRLEVBQUUsR0FBRyxJQUFJLENBQUMsS0FBSyxJQUFJLElBQUksQ0FBQyxXQUFXLElBQUksRUFBRSxJQUFJLElBQUksQ0FBQyxLQUFLLElBQUksRUFBRSxFQUFFLENBQUMsQ0FBQztJQUM5RixJQUFJLElBQUksQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUNoQixNQUFNLE1BQU0sR0FBRyxTQUFTLENBQWEsSUFBSSxDQUFDLFFBQVEsRUFBRSxHQUFHLENBQUMsQ0FBQztRQUN6RCxJQUFJLE1BQU07WUFBRSxPQUFPLE1BQU0sQ0FBQztJQUM5QixDQUFDO0lBRUQsTUFBTSxNQUFNLEdBQTJCO1FBQ25DLENBQUMsRUFBRSxJQUFJLENBQUMsS0FBSztRQUNiLEtBQUssRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLEtBQUssSUFBSSxFQUFFLENBQUM7UUFDL0IsY0FBYyxFQUFFLEdBQUc7S0FDdEIsQ0FBQztJQUNGLElBQUksSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDO1FBQ25CLE1BQU0sQ0FBQyxZQUFZLEdBQUcsSUFBSSxDQUFDLFdBQVcsQ0FBQztJQUMzQyxDQUFDO0lBRUQsTUFBTSxHQUFHLEdBQUcsTUFBTSxjQUFjLENBQUMsU0FBUyxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBRXBELE1BQU0sT0FBTyxHQUFlLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFNLEVBQUUsRUFBRSxDQUFDLENBQUM7UUFDN0MsT0FBTyxFQUFFLENBQUMsQ0FBQyxRQUFRO1FBQ25CLFdBQVcsRUFBRSxDQUFDLENBQUMsWUFBWTtRQUMzQixJQUFJLEVBQUUsQ0FBQyxDQUFDLElBQUksSUFBSSxDQUFDLENBQUMsWUFBWSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDNUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxJQUFJO1FBQ1osS0FBSyxFQUFFLENBQUMsQ0FBQyxLQUFLO1FBQ2QsR0FBRyxFQUFFLFVBQVUsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDO1FBQ3RCLEdBQUcsRUFBRSxVQUFVLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQztRQUN0QixPQUFPLEVBQUUsQ0FBQyxDQUFDLE9BQU87S0FDckIsQ0FBQyxDQUFDLENBQUM7SUFFSixJQUFJLElBQUksQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUNoQixVQUFVLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxHQUFHLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDNUMsQ0FBQztJQUVELE9BQU8sT0FBTyxDQUFDO0FBQ25CLENBQUM7QUFFRDs7Ozs7OztHQU9HO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxnQkFBZ0IsQ0FBQyxJQUE2QjtJQUNoRSxNQUFNLEdBQUcsR0FBRyxRQUFRLENBQUMsZUFBZSxFQUFFLEdBQUcsSUFBSSxDQUFDLElBQUksSUFBSSxJQUFJLENBQUMsT0FBTyxJQUFJLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDNUUsSUFBSSxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUM7UUFDaEIsTUFBTSxNQUFNLEdBQUcsU0FBUyxDQUFhLElBQUksQ0FBQyxRQUFRLEVBQUUsR0FBRyxDQUFDLENBQUM7UUFDekQsSUFBSSxNQUFNO1lBQUUsT0FBTyxNQUFNLENBQUM7SUFDOUIsQ0FBQztJQUVELDJFQUEyRTtJQUMzRSxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsT0FBTztRQUMzQixDQUFDLENBQUMsSUFBSSxJQUFJLENBQUMsSUFBSSxPQUFPLElBQUksQ0FBQyxPQUFPLEdBQUc7UUFDckMsQ0FBQyxDQUFDLElBQUksSUFBSSxDQUFDLElBQUksR0FBRyxDQUFDO0lBRXZCLE1BQU0sYUFBYSxHQUFHOztZQUVkLFVBQVU7Ozs7Ozs7O0NBUXJCLENBQUMsSUFBSSxFQUFFLENBQUM7SUFFTCxNQUFNLEdBQUcsR0FBRyxNQUFNLEtBQUssQ0FBQyx5Q0FBeUMsRUFBRTtRQUMvRCxNQUFNLEVBQUUsTUFBTTtRQUNkLE9BQU8sRUFBRTtZQUNMLGNBQWMsRUFBRSxtQ0FBbUM7WUFDbkQsWUFBWSxFQUFFLFVBQVU7U0FDM0I7UUFDRCxJQUFJLEVBQUUsUUFBUSxrQkFBa0IsQ0FBQyxhQUFhLENBQUMsRUFBRTtLQUNwRCxDQUFDLENBQUM7SUFFSCxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsRUFBRSxDQUFDO1FBQ1YsTUFBTSxJQUFJLEtBQUssQ0FBQyxnQkFBZ0IsR0FBRyxDQUFDLE1BQU0sS0FBSyxHQUFHLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FBQztJQUNyRSxDQUFDO0lBRUQsTUFBTSxJQUFJLEdBQUcsTUFBTSxHQUFHLENBQUMsSUFBSSxFQUFFLENBQUM7SUFFOUIsTUFBTSxPQUFPLEdBQWUsQ0FBQyxJQUFJLENBQUMsUUFBUSxJQUFJLEVBQUUsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQU8sRUFBRSxFQUFFLENBQUMsQ0FBQztRQUNoRSxPQUFPLEVBQUUsRUFBRSxDQUFDLEVBQUU7UUFDZCxXQUFXLEVBQUUsR0FBRyxFQUFFLENBQUMsSUFBSSxFQUFFLElBQUksSUFBSSxTQUFTLEtBQUssSUFBSSxDQUFDLElBQUksRUFBRTtRQUMxRCxJQUFJLEVBQUUsRUFBRSxDQUFDLElBQUksRUFBRSxJQUFJLElBQUksU0FBUztRQUNoQyxJQUFJLEVBQUUsRUFBRSxDQUFDLElBQUksRUFBRSxLQUFLLElBQUksU0FBUztRQUNqQyxLQUFLLEVBQUUsT0FBTztRQUNkLEdBQUcsRUFBRSxFQUFFLENBQUMsR0FBRztRQUNYLEdBQUcsRUFBRSxFQUFFLENBQUMsR0FBRztRQUNYLE9BQU8sRUFBRTtZQUNMLE1BQU0sRUFBRSxFQUFFLENBQUMsSUFBSSxFQUFFLEtBQUssS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLENBQUMsQ0FBQyxTQUFTO1lBQy9ELElBQUksRUFBRSxJQUFJLENBQUMsSUFBSTtZQUNmLE9BQU8sRUFBRSxJQUFJLENBQUMsT0FBTztTQUN4QjtLQUNKLENBQUMsQ0FBQyxDQUFDO0lBRUosc0JBQXNCO0lBQ3RCLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztJQUVyRCxJQUFJLElBQUksQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUNoQixVQUFVLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxHQUFHLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDNUMsQ0FBQztJQUVELE9BQU8sT0FBTyxDQUFDO0FBQ25CLENBQUMifQ==