@mailwoman/un-locode-lookup 4.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @mailwoman/un-locode-lookup
2
+
3
+ Place → **UN/LOCODE** (the UNECE Code for Trade and Transport Locations — `US NYC`, `NL RTM`). Look up
4
+ by country + place name, or by nearest coordinate. `node:sqlite` over the UNECE code list. An
5
+ [`@mailwoman/annotations`](../annotations) `Annotator`.
6
+
7
+ ## Build the DB
8
+
9
+ ```bash
10
+ # from the UNECE code list (e.g. datasets/un-locode code-list.csv)
11
+ npx @mailwoman/un-locode-lookup build --csv code-list.csv --out un-locode.db
12
+ ```
13
+
14
+ ## Look up
15
+
16
+ ```bash
17
+ npx @mailwoman/un-locode-lookup --db un-locode.db --country NL --name "Rotterdam"
18
+ # {"unLocode":"NL RTM"}
19
+
20
+ npx @mailwoman/un-locode-lookup --db un-locode.db --near 40.7128 -74.0060
21
+ # {"unLocode":"US NYC"}
22
+ ```
23
+
24
+ ## Library
25
+
26
+ ```ts
27
+ import { UnLocodeLookup, makeUnLocodeAnnotator } from "@mailwoman/un-locode-lookup"
28
+
29
+ const lookup = new UnLocodeLookup({ databasePath: "un-locode.db" })
30
+ lookup.byName("US", "New York") // "US NYC"
31
+ lookup.nearest(51.92, 4.48) // "NL RTM"
32
+
33
+ const annotator = makeUnLocodeAnnotator(lookup) // fills AnnotationSet.unLocode
34
+ ```
35
+
36
+ Name matching is diacritic-folded and case-insensitive. ~80% of UN/LOCODE entries carry coordinates
37
+ (93k of 116k), so `nearest` is broadly useful; `byName` is exact. Data: UNECE UN/LOCODE (public domain
38
+ code list).
package/out/build.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Build the UN/LOCODE lookup DB from the UNECE code list CSV (datasets/un-locode `code-list.csv`:
7
+ * columns Change, Country, Location, Name, NameWoDiacritics, Subdivision, Status, Function, Date,
8
+ * IATA, Coordinates, Remarks). One row per assigned location; coordinates parsed where present.
9
+ */
10
+ /** Read the code-list CSV at `csvPath` and write the lookup DB to `dbPath`. */
11
+ export declare function buildUnLocodeDb(csvPath: string, dbPath: string): {
12
+ rows: number;
13
+ withCoords: number;
14
+ };
15
+ //# sourceMappingURL=build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../build.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAeH,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CA2BrG"}
package/out/build.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Build the UN/LOCODE lookup DB from the UNECE code list CSV (datasets/un-locode `code-list.csv`:
7
+ * columns Change, Country, Location, Name, NameWoDiacritics, Subdivision, Status, Function, Date,
8
+ * IATA, Coordinates, Remarks). One row per assigned location; coordinates parsed where present.
9
+ */
10
+ import { parse } from "csv-parse/sync";
11
+ import { readFileSync } from "node:fs";
12
+ import { DatabaseSync } from "node:sqlite";
13
+ import { foldName, parseUnLocodeCoords } from "./index.js";
14
+ /** Read the code-list CSV at `csvPath` and write the lookup DB to `dbPath`. */
15
+ export function buildUnLocodeDb(csvPath, dbPath) {
16
+ const records = parse(readFileSync(csvPath), {
17
+ columns: true,
18
+ skip_empty_lines: true,
19
+ relax_quotes: true,
20
+ });
21
+ const db = new DatabaseSync(dbPath);
22
+ db.exec("DROP TABLE IF EXISTS un_locode");
23
+ db.exec("CREATE TABLE un_locode (country TEXT NOT NULL, location TEXT NOT NULL, name TEXT, nameNorm TEXT, lat REAL, lon REAL)");
24
+ const insert = db.prepare("INSERT INTO un_locode (country, location, name, nameNorm, lat, lon) VALUES (?,?,?,?,?,?)");
25
+ let withCoords = 0;
26
+ db.exec("BEGIN");
27
+ for (const r of records) {
28
+ if (!r.Country || !r.Location)
29
+ continue; // header/country rows carry no Location
30
+ const coords = r.Coordinates ? parseUnLocodeCoords(r.Coordinates) : null;
31
+ if (coords)
32
+ withCoords++;
33
+ const name = r.NameWoDiacritics || r.Name || "";
34
+ insert.run(r.Country, r.Location, r.Name || name, foldName(name), coords?.lat ?? null, coords?.lon ?? null);
35
+ }
36
+ db.exec("COMMIT");
37
+ db.exec("CREATE INDEX idx_locode_name ON un_locode (country, nameNorm)");
38
+ db.exec("CREATE INDEX idx_locode_bbox ON un_locode (lat, lon)");
39
+ db.close();
40
+ return { rows: records.length, withCoords };
41
+ }
42
+ //# sourceMappingURL=build.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../build.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAU1D,+EAA+E;AAC/E,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,MAAc;IAC9D,MAAM,OAAO,GAAG,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE;QAC5C,OAAO,EAAE,IAAI;QACb,gBAAgB,EAAE,IAAI;QACtB,YAAY,EAAE,IAAI;KAClB,CAAa,CAAA;IACd,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAA;IACnC,EAAE,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAA;IACzC,EAAE,CAAC,IAAI,CACN,sHAAsH,CACtH,CAAA;IACD,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC,0FAA0F,CAAC,CAAA;IAErH,IAAI,UAAU,GAAG,CAAC,CAAA;IAClB,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAChB,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACzB,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ;YAAE,SAAQ,CAAC,wCAAwC;QAChF,MAAM,MAAM,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QACxE,IAAI,MAAM;YAAE,UAAU,EAAE,CAAA;QACxB,MAAM,IAAI,GAAG,CAAC,CAAC,gBAAgB,IAAI,CAAC,CAAC,IAAI,IAAI,EAAE,CAAA;QAC/C,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,IAAI,IAAI,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,CAAA;IAC5G,CAAC;IACD,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACjB,EAAE,CAAC,IAAI,CAAC,+DAA+D,CAAC,CAAA;IACxE,EAAE,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAA;IAC/D,EAAE,CAAC,KAAK,EAAE,CAAA;IACV,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,CAAA;AAC5C,CAAC"}
package/out/cli.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @copyright Sister Software
4
+ * @license AGPL-3.0
5
+ * @author Teffen Ellis, et al.
6
+ *
7
+ * `mailwoman-un-locode` — build the lookup DB, or look up a UN/LOCODE by name or coordinate.
8
+ *
9
+ * Mailwoman-un-locode build --csv code-list.csv --out un-locode.db mailwoman-un-locode --db
10
+ * un-locode.db --country US --name "New York" mailwoman-un-locode --db un-locode.db --near
11
+ * 40.7128 -74.0060
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG"}
package/out/cli.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @copyright Sister Software
4
+ * @license AGPL-3.0
5
+ * @author Teffen Ellis, et al.
6
+ *
7
+ * `mailwoman-un-locode` — build the lookup DB, or look up a UN/LOCODE by name or coordinate.
8
+ *
9
+ * Mailwoman-un-locode build --csv code-list.csv --out un-locode.db mailwoman-un-locode --db
10
+ * un-locode.db --country US --name "New York" mailwoman-un-locode --db un-locode.db --near
11
+ * 40.7128 -74.0060
12
+ */
13
+ import { parseArgs } from "node:util";
14
+ import { buildUnLocodeDb } from "./build.js";
15
+ import { UnLocodeLookup } from "./index.js";
16
+ if (process.argv[2] === "build") {
17
+ const { values } = parseArgs({
18
+ args: process.argv.slice(3),
19
+ options: { csv: { type: "string" }, out: { type: "string" } },
20
+ });
21
+ if (!values.csv || !values.out) {
22
+ console.error("Usage: mailwoman-un-locode build --csv <code-list.csv> --out <db>");
23
+ process.exit(1);
24
+ }
25
+ const { rows, withCoords } = buildUnLocodeDb(values.csv, values.out);
26
+ console.error(`built ${values.out} (${rows} rows, ${withCoords} with coordinates)`);
27
+ }
28
+ else {
29
+ // Hand-parse so negative coordinates work as positionals.
30
+ const args = process.argv.slice(2);
31
+ let databasePath;
32
+ let country;
33
+ let name;
34
+ const near = [];
35
+ for (let i = 0; i < args.length; i++) {
36
+ if (args[i] === "--db")
37
+ databasePath = args[++i];
38
+ else if (args[i] === "--country")
39
+ country = args[++i];
40
+ else if (args[i] === "--name")
41
+ name = args[++i];
42
+ else if (args[i] === "--near")
43
+ continue;
44
+ else {
45
+ const n = Number(args[i]);
46
+ if (Number.isFinite(n))
47
+ near.push(n);
48
+ }
49
+ }
50
+ if (!databasePath) {
51
+ console.error("Usage: mailwoman-un-locode --db <db> (--country CC --name NAME | --near <lat> <lon>)");
52
+ process.exit(1);
53
+ }
54
+ const lookup = new UnLocodeLookup({ databasePath });
55
+ let code = null;
56
+ if (country && name)
57
+ code = lookup.byName(country, name);
58
+ else if (near.length === 2)
59
+ code = lookup.nearest(near[0], near[1]);
60
+ console.log(JSON.stringify({ unLocode: code }));
61
+ lookup.close();
62
+ }
63
+ //# sourceMappingURL=cli.js.map
package/out/cli.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
package/out/index.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/un-locode-lookup` — place → UN/LOCODE (UNECE Code for Trade and Transport Locations).
7
+ * Two ways in: by country + place name (exact, diacritic-folded), or by nearest coordinate (for
8
+ * the ~⅓ of entries that carry one). Backed by a `node:sqlite` table built from the UNECE code
9
+ * list. An `@mailwoman/annotations` `Annotator`.
10
+ */
11
+ import type { Annotator } from "@mailwoman/annotations";
12
+ import { DatabaseSync } from "node:sqlite";
13
+ /** Fold a place name to its match key: strip diacritics, lowercase, collapse whitespace. */
14
+ export declare function foldName(name: string): string;
15
+ /** Parse a UN/LOCODE coordinate (`"4923N 01522E"`) to decimal degrees, or null if absent/malformed. */
16
+ export declare function parseUnLocodeCoords(raw: string): {
17
+ lat: number;
18
+ lon: number;
19
+ } | null;
20
+ /** A UN/LOCODE lookup over a built `node:sqlite` table. */
21
+ export declare class UnLocodeLookup {
22
+ #private;
23
+ constructor(opts: {
24
+ databasePath: string;
25
+ } | {
26
+ database: DatabaseSync;
27
+ });
28
+ /** The UN/LOCODE (`"US NYC"`) for a country + place name, or null. */
29
+ byName(country: string, name: string): string | null;
30
+ /** The nearest coordinate-bearing UN/LOCODE within `maxKm`, or null. */
31
+ nearest(lat: number, lon: number, maxKm?: number): string | null;
32
+ close(): void;
33
+ }
34
+ /**
35
+ * Build an `Annotator` filling `AnnotationSet.unLocode`. Prefers a country + place-name match (when
36
+ * the resolver supplies them via `countryCode` / `placeName`), and falls back to the nearest
37
+ * coordinate.
38
+ */
39
+ export declare function makeUnLocodeAnnotator(lookup: UnLocodeLookup, opts?: {
40
+ maxKm?: number;
41
+ }): Annotator;
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,wBAAwB,CAAA;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE1C,4FAA4F;AAC5F,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,uGAAuG;AACvG,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAMpF;AAYD,2DAA2D;AAC3D,qBAAa,cAAc;;gBAKd,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,QAAQ,EAAE,YAAY,CAAA;KAAE;IAUvE,sEAAsE;IACtE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAOpD,wEAAwE;IACxE,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,GAAG,IAAI;IAiB5D,KAAK,IAAI,IAAI;CAGb;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,SAAS,CAMtG"}
package/out/index.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/un-locode-lookup` — place → UN/LOCODE (UNECE Code for Trade and Transport Locations).
7
+ * Two ways in: by country + place name (exact, diacritic-folded), or by nearest coordinate (for
8
+ * the ~⅓ of entries that carry one). Backed by a `node:sqlite` table built from the UNECE code
9
+ * list. An `@mailwoman/annotations` `Annotator`.
10
+ */
11
+ import { DatabaseSync } from "node:sqlite";
12
+ /** Fold a place name to its match key: strip diacritics, lowercase, collapse whitespace. */
13
+ export function foldName(name) {
14
+ return name.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/\s+/g, " ");
15
+ }
16
+ /** Parse a UN/LOCODE coordinate (`"4923N 01522E"`) to decimal degrees, or null if absent/malformed. */
17
+ export function parseUnLocodeCoords(raw) {
18
+ const m = raw.trim().match(/^(\d{2})(\d{2})([NS])\s+(\d{3})(\d{2})([EW])$/);
19
+ if (!m)
20
+ return null;
21
+ const lat = (Number(m[1]) + Number(m[2]) / 60) * (m[3] === "S" ? -1 : 1);
22
+ const lon = (Number(m[4]) + Number(m[5]) / 60) * (m[6] === "W" ? -1 : 1);
23
+ return { lat, lon };
24
+ }
25
+ const EARTH_R_KM = 6371;
26
+ function haversineKm(aLat, aLon, bLat, bLon) {
27
+ const dLat = ((bLat - aLat) * Math.PI) / 180;
28
+ const dLon = ((bLon - aLon) * Math.PI) / 180;
29
+ const s = Math.sin(dLat / 2) ** 2 +
30
+ Math.cos((aLat * Math.PI) / 180) * Math.cos((bLat * Math.PI) / 180) * Math.sin(dLon / 2) ** 2;
31
+ return 2 * EARTH_R_KM * Math.asin(Math.sqrt(s));
32
+ }
33
+ /** A UN/LOCODE lookup over a built `node:sqlite` table. */
34
+ export class UnLocodeLookup {
35
+ #db;
36
+ #byName;
37
+ #byBox;
38
+ constructor(opts) {
39
+ this.#db = "database" in opts ? opts.database : new DatabaseSync(opts.databasePath, { readOnly: true });
40
+ this.#byName = this.#db.prepare("SELECT country, location FROM un_locode WHERE country = ? AND nameNorm = ? LIMIT 1");
41
+ this.#byBox = this.#db.prepare("SELECT location, country, lat, lon FROM un_locode WHERE lat BETWEEN ? AND ? AND lon BETWEEN ? AND ?");
42
+ }
43
+ /** The UN/LOCODE (`"US NYC"`) for a country + place name, or null. */
44
+ byName(country, name) {
45
+ const row = this.#byName.get(country.toUpperCase(), foldName(name));
46
+ return row ? `${row.country} ${row.location}` : null;
47
+ }
48
+ /** The nearest coordinate-bearing UN/LOCODE within `maxKm`, or null. */
49
+ nearest(lat, lon, maxKm = 25) {
50
+ const dLat = maxKm / 111;
51
+ const dLon = maxKm / (111 * Math.max(0.01, Math.cos((lat * Math.PI) / 180)));
52
+ const rows = this.#byBox.all(lat - dLat, lat + dLat, lon - dLon, lon + dLon);
53
+ let best = null;
54
+ for (const r of rows) {
55
+ const km = haversineKm(lat, lon, r.lat, r.lon);
56
+ if (km <= maxKm && (!best || km < best.km))
57
+ best = { code: `${r.country} ${r.location}`, km };
58
+ }
59
+ return best?.code ?? null;
60
+ }
61
+ close() {
62
+ this.#db.close();
63
+ }
64
+ }
65
+ /**
66
+ * Build an `Annotator` filling `AnnotationSet.unLocode`. Prefers a country + place-name match (when
67
+ * the resolver supplies them via `countryCode` / `placeName`), and falls back to the nearest
68
+ * coordinate.
69
+ */
70
+ export function makeUnLocodeAnnotator(lookup, opts = {}) {
71
+ return ({ lat, lon, countryCode, placeName }) => {
72
+ const byName = countryCode && placeName ? lookup.byName(countryCode, placeName) : null;
73
+ const code = byName ?? lookup.nearest(lat, lon, opts.maxKm);
74
+ return code ? { unLocode: code } : {};
75
+ };
76
+ }
77
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@mailwoman/un-locode-lookup",
3
+ "version": "4.15.1",
4
+ "description": "Place → UN/LOCODE (UNECE Code for Trade and Transport Locations), by country + name or nearest coordinate. node:sqlite over the UNECE code list. An @mailwoman/annotations Annotator.",
5
+ "license": "AGPL-3.0-only",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/sister-software/mailwoman.git",
9
+ "directory": "un-locode-lookup"
10
+ },
11
+ "type": "module",
12
+ "exports": {
13
+ "./package.json": "./package.json",
14
+ ".": "./out/index.js",
15
+ "./build": "./out/build.js"
16
+ },
17
+ "dependencies": {
18
+ "@mailwoman/annotations": "4.15.1",
19
+ "csv-parse": "^5.6.0"
20
+ },
21
+ "files": [
22
+ "out/**/*.js",
23
+ "out/**/*.js.map",
24
+ "out/**/*.d.ts",
25
+ "out/**/*.d.ts.map",
26
+ "README.md"
27
+ ],
28
+ "bin": "./out/cli.js",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }