← back to Costa Rica
scripts/cr-geocode-regions.js
80 lines
'use strict';
// Fill missing region centroids via Nominatim (free, 1req/s etiquette),
// then re-validate the '-nogeo' OSM website matches: re-derive each match
// from the cached OSM dataset and geo-check with the fresh centroid.
// >60km → purge the website (bad match); ≤60km → drop the -nogeo tag.
const fs = require('fs');
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const UA = { 'User-Agent': 'CR-Directory/1.0 (costarica.agentabrams.com; info@agentabrams.com)' };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const norm = (s) => (s || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
.replace(/\b(sociedad anonima|s\.?a\.?|ltda\.?|s\.?r\.?l\.?|inc\.?|cia\.?)\b/g, '')
.replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
const km = (a, b, c, d) => {
const R = 6371, dLat = (c - a) * Math.PI / 180, dLon = (d - b) * Math.PI / 180;
const x = Math.sin(dLat / 2) ** 2 + Math.cos(a * Math.PI / 180) * Math.cos(c * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(x));
};
(async () => {
// 1) geocode regions missing centroids
const { rows: regions } = await pool.query(
`SELECT id, slug, name, province FROM regions WHERE lat IS NULL`);
console.log(`regions missing centroid: ${regions.length}`);
let geocoded = 0;
for (const r of regions) {
const q = encodeURIComponent(`${r.name}, ${r.province || ''}, Costa Rica`);
try {
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&limit=1&countrycodes=cr&q=${q}`, { headers: UA });
if (res.ok) {
const j = await res.json();
if (j[0]) {
await pool.query(`UPDATE regions SET lat=$1, lng=$2 WHERE id=$3`, [j[0].lat, j[0].lon, r.id]);
geocoded++;
} else console.log(`no hit: ${r.slug}`);
} else console.log(`HTTP ${res.status}: ${r.slug}`);
} catch (e) { console.log(`err ${r.slug}: ${e.message}`); }
await sleep(1100);
}
console.log(`geocoded ${geocoded}/${regions.length}`);
// 2) re-validate -nogeo matches with fresh centroids
const osm = JSON.parse(fs.readFileSync(__dirname + '/data/cache/osm-cr-websites.json', 'utf8')).elements;
const byName = new Map();
for (const e of osm) {
const t = e.tags || {}; const n = norm(t.name);
if (n.length < 4) continue;
const lat = e.lat ?? e.center?.lat, lon = e.lon ?? e.center?.lon;
const w = t.website || t['contact:website'];
if (!byName.has(n)) byName.set(n, []);
byName.get(n).push({ website: w, lat, lon });
}
const { rows: nogeo } = await pool.query(`
SELECT p.id, p.name, p.website, p.website_source, r.lat AS rlat, r.lng AS rlng
FROM places p LEFT JOIN regions r ON r.id = p.region_id
WHERE p.website_source LIKE 'osm%nogeo%'`);
console.log(`-nogeo matches to re-validate: ${nogeo.length}`);
let confirmed = 0, purged = 0, still = 0;
for (const p of nogeo) {
if (!p.rlat) { still++; continue; } // region still has no centroid
// find the OSM record whose website matches what we stored
let cand = null;
for (const list of byName.values())
for (const c of list) if (c.website === p.website && c.lat) { cand = c; break; }
if (!cand) { still++; continue; }
if (km(Number(p.rlat), Number(p.rlng), cand.lat, cand.lon) > 60) {
await pool.query(
`UPDATE places SET website=NULL, website_source=NULL WHERE id=$1`, [p.id]);
purged++;
} else {
await pool.query(
`UPDATE places SET website_source=replace(website_source,'-nogeo','') WHERE id=$1`, [p.id]);
confirmed++;
}
}
console.log(`REVALIDATED confirmed=${confirmed} purged=${purged} unresolved=${still}`);
await pool.end();
})();