Files
Jyotisha/frontend/scripts/pull-global-locations.mjs
T

80 lines
3.5 KiB
JavaScript

import { gunzipSync } from "node:zlib";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const RELEASE = "v3.2-export.7";
const SOURCE_URL = `https://github.com/dr5hn/countries-states-cities-database/releases/download/${RELEASE}/json-countries%2Bstates%2Bcities.json.gz`;
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const outputDir = resolve(root, "public/data/world-locations");
const indexPath = resolve(root, "src/data/world-countries.json");
async function loadSource() {
const localPath = process.env.GLOBAL_LOCATION_SOURCE_FILE;
if (localPath) return JSON.parse(await readFile(localPath, "utf8"));
const response = await fetch(SOURCE_URL, { headers: { "user-agent": "Jyotisha location-data refresh" } });
if (!response.ok) throw new Error(`Unable to download global location data: ${response.status} ${response.statusText}`);
return JSON.parse(gunzipSync(Buffer.from(await response.arrayBuffer())).toString("utf8"));
}
function coordinate(value, label) {
const result = Number(value);
if (!Number.isFinite(result)) throw new Error(`Missing valid ${label}: ${value}`);
return result;
}
function regionCode(state) {
return String(state.iso3166_2 || state.iso2 || state.id);
}
function compactCity(city) {
return {
code: String(city.id),
name: city.name,
nativeName: typeof city.native === "string" && city.native.trim() ? city.native : undefined,
latitude: coordinate(city.latitude, `latitude for ${city.name}`),
longitude: coordinate(city.longitude, `longitude for ${city.name}`),
timezone: city.timezone,
};
}
function compactRegion(state) {
return {
code: regionCode(state),
name: state.name,
nativeName: typeof state.native === "string" && state.native.trim() ? state.native : undefined,
latitude: coordinate(state.latitude, `latitude for ${state.name}`),
longitude: coordinate(state.longitude, `longitude for ${state.name}`),
timezone: state.timezone,
cities: (Array.isArray(state.cities) ? state.cities : []).map(compactCity),
};
}
const countries = await loadSource();
if (!Array.isArray(countries)) throw new Error("Global location source is not an array");
await mkdir(outputDir, { recursive: true });
const index = [];
let stateCount = 0;
let cityCount = 0;
for (const country of countries) {
const code = typeof country.iso2 === "string" ? country.iso2.trim().toUpperCase() : "";
if (!/^[A-Z]{2}$/.test(code)) continue;
const regions = (Array.isArray(country.states) ? country.states : []).map(compactRegion);
if (regions.length === 0) continue;
const payload = {
code,
name: country.name,
nativeName: typeof country.native === "string" && country.native.trim() ? country.native : undefined,
regions,
};
await writeFile(resolve(outputDir, `${code.toLowerCase()}.json`), `${JSON.stringify(payload)}\n`, "utf8");
index.push({ code, name: country.name, nativeName: payload.nativeName, regionCount: regions.length });
stateCount += regions.length;
cityCount += regions.reduce((total, region) => total + region.cities.length, 0);
}
index.sort((a, b) => a.name.localeCompare(b.name));
await writeFile(indexPath, `${JSON.stringify({ source: { repository: "dr5hn/countries-states-cities-database", release: RELEASE, url: SOURCE_URL, license: "ODbL-1.0" }, countries: index })}\n`, "utf8");
console.log(`Wrote ${indexPath}: ${index.length} countries, ${stateCount} regions, ${cityCount} cities.`);