80 lines
3.5 KiB
JavaScript
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.`);
|