/** * Reads every border post OpenStreetMap knows - the nodes, ways and * relations tagged barrier=border_control - through the public Overpass API, * in tiles of the world, gently, and keeps each answer in scripts/.osm-cache/border-crossings * (not committed) so an interrupted run carries on where it stopped. * * npm run crossings:fetch * * Then scripts/build-border-crossings.mjs makes the file the map reads. * The data is OpenStreetMap's, ODbL-licensed and credited in the app; * nothing about anyone is sent. The world has about 8,200 such nodes and 1,400 * ways and relations, two megabytes in all. */ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = join(dirname(fileURLToPath(import.meta.url)), '..'); export const CROSSINGS_CACHE = join(root, 'scripts/.osm-cache/border-crossings'); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); /** The tiles of the world: thirty degrees of longitude by twenty of latitude, from 60° south to 85° north. */ export function crossingTiles() { const tiles = []; for (let south = -60; south < 85; south += 20) { for (let west = -180; west < 180; west += 30) tiles.push([south, west, Math.min(south + 20, 85), west + 30]); } return tiles; } async function ask(tile) { const [south, west, north, east] = tile; const query = `[out:json][timeout:60];nwr["barrier"="border_control"](${south},${west},${north},${east});out center;`; for (let attempt = 1; attempt <= 6; attempt += 1) { try { const response = await fetch('https://overpass-api.de/api/interpreter', { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', 'user-agent': '197-app border crossings build (197.world)' }, body: `data=${encodeURIComponent(query)}`, signal: AbortSignal.timeout(90_000), }); const text = await response.text(); if (response.ok && text.startsWith('{')) return JSON.parse(text); } catch { // The public server is busy at times: wait, and ask again. } await sleep(8_000 * attempt); } return undefined; } if (process.argv[1] === fileURLToPath(import.meta.url)) { mkdirSync(CROSSINGS_CACHE, { recursive: true }); const failed = []; let done = 0; for (const tile of crossingTiles()) { const file = join(CROSSINGS_CACHE, `tile_${tile.join('_')}.json`); if (existsSync(file)) { done += 1; continue; } const answer = await ask(tile); if (!answer) { failed.push(tile.join(',')); continue; } writeFileSync(file, JSON.stringify(answer.elements ?? [])); done += 1; await sleep(2_500); } console.log(JSON.stringify({ done, failed })); if (failed.length > 0) process.exitCode = 1; }