@mailwoman/nuts-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 +33 -0
- package/out/build.d.ts +14 -0
- package/out/build.d.ts.map +1 -0
- package/out/build.js +51 -0
- package/out/build.js.map +1 -0
- package/out/cli.d.ts +13 -0
- package/out/cli.d.ts.map +1 -0
- package/out/cli.js +51 -0
- package/out/cli.js.map +1 -0
- package/out/index.d.ts +36 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +80 -0
- package/out/index.js.map +1 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @mailwoman/nuts-lookup
|
|
2
|
+
|
|
3
|
+
EU coordinate → **NUTS** statistical-region codes (levels 1–3 — the way OpenCage returns them).
|
|
4
|
+
Point-in-polygon over the [Eurostat GISCO](https://ec.europa.eu/eurostat/web/gisco) NUTS boundaries in a
|
|
5
|
+
`node:sqlite` table. An [`@mailwoman/annotations`](../annotations) `Annotator`.
|
|
6
|
+
|
|
7
|
+
## Build the DB
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# from a Eurostat GISCO NUTS GeoJSON (e.g. NUTS_RG_03M_2021_4326.geojson)
|
|
11
|
+
npx @mailwoman/nuts-lookup build --geojson NUTS_RG_03M_2021_4326.geojson --out nuts.db
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Look up
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx @mailwoman/nuts-lookup --db nuts.db 52.52 13.405
|
|
18
|
+
# {"nuts":{"level1":"DE3","level2":"DE30","level3":"DE300"}} (Berlin)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Library
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { NutsLookup, makeNutsAnnotator } from "@mailwoman/nuts-lookup"
|
|
25
|
+
|
|
26
|
+
const lookup = new NutsLookup({ databasePath: "nuts.db" })
|
|
27
|
+
lookup.find(52.52, 13.405) // { level1: "DE3", level2: "DE30", level3: "DE300" }
|
|
28
|
+
|
|
29
|
+
const annotator = makeNutsAnnotator(lookup) // fills AnnotationSet.nuts (EU only; abstains elsewhere)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
NUTS ids nest by prefix, so the lookup finds the deepest containing region and derives its parents.
|
|
33
|
+
Data: Eurostat GISCO NUTS (© EuroGeographics for the administrative boundaries).
|
package/out/build.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Build the NUTS polygon DB from a Eurostat GISCO NUTS GeoJSON (`NUTS_RG_*_4326.geojson`: features
|
|
7
|
+
* of `{ properties: { NUTS_ID, LEVL_CODE }, geometry: Polygon|MultiPolygon }`). One row per
|
|
8
|
+
* region, with its level and bounding box for the lookup's prefilter.
|
|
9
|
+
*/
|
|
10
|
+
/** Read the NUTS GeoJSON at `geojsonPath` and write the polygon DB to `dbPath`. */
|
|
11
|
+
export declare function buildNutsDb(geojsonPath: string, dbPath: string): {
|
|
12
|
+
regions: number;
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../build.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CA+CpF"}
|
package/out/build.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Build the NUTS polygon DB from a Eurostat GISCO NUTS GeoJSON (`NUTS_RG_*_4326.geojson`: features
|
|
7
|
+
* of `{ properties: { NUTS_ID, LEVL_CODE }, geometry: Polygon|MultiPolygon }`). One row per
|
|
8
|
+
* region, with its level and bounding box for the lookup's prefilter.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { DatabaseSync } from "node:sqlite";
|
|
12
|
+
/** Read the NUTS GeoJSON at `geojsonPath` and write the polygon DB to `dbPath`. */
|
|
13
|
+
export function buildNutsDb(geojsonPath, dbPath) {
|
|
14
|
+
const data = JSON.parse(readFileSync(geojsonPath, "utf8"));
|
|
15
|
+
const db = new DatabaseSync(dbPath);
|
|
16
|
+
db.exec("DROP TABLE IF EXISTS nuts_regions");
|
|
17
|
+
db.exec("CREATE TABLE nuts_regions (nutsId TEXT NOT NULL, level INTEGER, minLat REAL, maxLat REAL, minLon REAL, maxLon REAL, geom TEXT NOT NULL)");
|
|
18
|
+
const insert = db.prepare("INSERT INTO nuts_regions (nutsId, level, minLat, maxLat, minLon, maxLon, geom) VALUES (?,?,?,?,?,?,?)");
|
|
19
|
+
db.exec("BEGIN");
|
|
20
|
+
for (const feature of data.features) {
|
|
21
|
+
const polygons = feature.geometry.type === "Polygon"
|
|
22
|
+
? [feature.geometry.coordinates]
|
|
23
|
+
: feature.geometry.coordinates;
|
|
24
|
+
let minLat = 90;
|
|
25
|
+
let maxLat = -90;
|
|
26
|
+
let minLon = 180;
|
|
27
|
+
let maxLon = -180;
|
|
28
|
+
for (const polygon of polygons) {
|
|
29
|
+
for (const ring of polygon) {
|
|
30
|
+
for (const point of ring) {
|
|
31
|
+
const lon = point[0];
|
|
32
|
+
const lat = point[1];
|
|
33
|
+
if (lat < minLat)
|
|
34
|
+
minLat = lat;
|
|
35
|
+
if (lat > maxLat)
|
|
36
|
+
maxLat = lat;
|
|
37
|
+
if (lon < minLon)
|
|
38
|
+
minLon = lon;
|
|
39
|
+
if (lon > maxLon)
|
|
40
|
+
maxLon = lon;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
insert.run(feature.properties.NUTS_ID, feature.properties.LEVL_CODE, minLat, maxLat, minLon, maxLon, JSON.stringify(polygons));
|
|
45
|
+
}
|
|
46
|
+
db.exec("COMMIT");
|
|
47
|
+
db.exec("CREATE INDEX idx_nuts_level_bbox ON nuts_regions (level, minLat, maxLat, minLon, maxLon)");
|
|
48
|
+
db.close();
|
|
49
|
+
return { regions: data.features.length };
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=build.js.map
|
package/out/build.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../build.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAQ1C,mFAAmF;AACnF,MAAM,UAAU,WAAW,CAAC,WAAmB,EAAE,MAAc;IAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAgC,CAAA;IACzF,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAA;IACnC,EAAE,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAA;IAC5C,EAAE,CAAC,IAAI,CACN,yIAAyI,CACzI,CAAA;IACD,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CACxB,uGAAuG,CACvG,CAAA;IAED,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAChB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QACrC,MAAM,QAAQ,GACb,OAAO,CAAC,QAAQ,CAAC,IAAI,KAAK,SAAS;YAClC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,WAA2B,CAAC;YAChD,CAAC,CAAE,OAAO,CAAC,QAAQ,CAAC,WAA8B,CAAA;QACpD,IAAI,MAAM,GAAG,EAAE,CAAA;QACf,IAAI,MAAM,GAAG,CAAC,EAAE,CAAA;QAChB,IAAI,MAAM,GAAG,GAAG,CAAA;QAChB,IAAI,MAAM,GAAG,CAAC,GAAG,CAAA;QACjB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAChC,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;gBAC5B,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;oBAC1B,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAE,CAAA;oBACrB,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAE,CAAA;oBACrB,IAAI,GAAG,GAAG,MAAM;wBAAE,MAAM,GAAG,GAAG,CAAA;oBAC9B,IAAI,GAAG,GAAG,MAAM;wBAAE,MAAM,GAAG,GAAG,CAAA;oBAC9B,IAAI,GAAG,GAAG,MAAM;wBAAE,MAAM,GAAG,GAAG,CAAA;oBAC9B,IAAI,GAAG,GAAG,MAAM;wBAAE,MAAM,GAAG,GAAG,CAAA;gBAC/B,CAAC;YACF,CAAC;QACF,CAAC;QACD,MAAM,CAAC,GAAG,CACT,OAAO,CAAC,UAAU,CAAC,OAAO,EAC1B,OAAO,CAAC,UAAU,CAAC,SAAS,EAC5B,MAAM,EACN,MAAM,EACN,MAAM,EACN,MAAM,EACN,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CACxB,CAAA;IACF,CAAC;IACD,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACjB,EAAE,CAAC,IAAI,CAAC,0FAA0F,CAAC,CAAA;IACnG,EAAE,CAAC,KAAK,EAAE,CAAA;IACV,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAA;AACzC,CAAC"}
|
package/out/cli.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @copyright Sister Software
|
|
4
|
+
* @license AGPL-3.0
|
|
5
|
+
* @author Teffen Ellis, et al.
|
|
6
|
+
*
|
|
7
|
+
* `mailwoman-nuts` — build the NUTS polygon DB, or look up a coordinate's NUTS codes.
|
|
8
|
+
*
|
|
9
|
+
* Mailwoman-nuts build --geojson NUTS_RG_03M_2021_4326.geojson --out nuts.db mailwoman-nuts --db
|
|
10
|
+
* nuts.db 52.52 13.405
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=cli.d.ts.map
|
package/out/cli.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;GASG"}
|
package/out/cli.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @copyright Sister Software
|
|
4
|
+
* @license AGPL-3.0
|
|
5
|
+
* @author Teffen Ellis, et al.
|
|
6
|
+
*
|
|
7
|
+
* `mailwoman-nuts` — build the NUTS polygon DB, or look up a coordinate's NUTS codes.
|
|
8
|
+
*
|
|
9
|
+
* Mailwoman-nuts build --geojson NUTS_RG_03M_2021_4326.geojson --out nuts.db mailwoman-nuts --db
|
|
10
|
+
* nuts.db 52.52 13.405
|
|
11
|
+
*/
|
|
12
|
+
import { parseArgs } from "node:util";
|
|
13
|
+
import { buildNutsDb } from "./build.js";
|
|
14
|
+
import { NutsLookup } from "./index.js";
|
|
15
|
+
if (process.argv[2] === "build") {
|
|
16
|
+
const { values } = parseArgs({
|
|
17
|
+
args: process.argv.slice(3),
|
|
18
|
+
options: { geojson: { type: "string" }, out: { type: "string" } },
|
|
19
|
+
});
|
|
20
|
+
if (!values.geojson || !values.out) {
|
|
21
|
+
console.error("Usage: mailwoman-nuts build --geojson <path> --out <db>");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const { regions } = buildNutsDb(values.geojson, values.out);
|
|
25
|
+
console.error(`built ${values.out} (${regions} regions)`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Hand-parse so negative coordinates work as positionals.
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
let databasePath;
|
|
31
|
+
const coords = [];
|
|
32
|
+
for (let i = 0; i < args.length; i++) {
|
|
33
|
+
if (args[i] === "--db")
|
|
34
|
+
databasePath = args[++i];
|
|
35
|
+
else {
|
|
36
|
+
const n = Number(args[i]);
|
|
37
|
+
if (Number.isFinite(n))
|
|
38
|
+
coords.push(n);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const lat = coords[0];
|
|
42
|
+
const lon = coords[1];
|
|
43
|
+
if (!databasePath || lat == null || lon == null) {
|
|
44
|
+
console.error("Usage: mailwoman-nuts --db <db> <lat> <lon>");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const lookup = new NutsLookup({ databasePath });
|
|
48
|
+
console.log(JSON.stringify({ nuts: lookup.find(lat, lon) }));
|
|
49
|
+
lookup.close();
|
|
50
|
+
}
|
|
51
|
+
//# 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":";AACA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEvC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,EAAE,CAAC;IACjC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QAC5B,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3B,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;KACjE,CAAC,CAAA;IACF,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QACpC,OAAO,CAAC,KAAK,CAAC,yDAAyD,CAAC,CAAA;QACxE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC;IACD,MAAM,EAAE,OAAO,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;IAC3D,OAAO,CAAC,KAAK,CAAC,SAAS,MAAM,CAAC,GAAG,KAAK,OAAO,WAAW,CAAC,CAAA;AAC1D,CAAC;KAAM,CAAC;IACP,0DAA0D;IAC1D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAClC,IAAI,YAAgC,CAAA;IACpC,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM;YAAE,YAAY,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;aAC3C,CAAC;YACL,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;YACzB,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACvC,CAAC;IACF,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;IACrB,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;IACrB,IAAI,CAAC,YAAY,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;QACjD,OAAO,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAA;QAC5D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC,CAAA;IAC/C,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IAC5D,MAAM,CAAC,KAAK,EAAE,CAAA;AACf,CAAC"}
|
package/out/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `@mailwoman/nuts-lookup` — EU coordinate → NUTS statistical-region codes (levels 1–3). Point-in-
|
|
7
|
+
* polygon over the Eurostat GISCO NUTS boundaries in a `node:sqlite` table. NUTS ids nest by
|
|
8
|
+
* prefix (`DE` → `DE1` → `DE11` → `DE111`), so we find the deepest containing region and derive
|
|
9
|
+
* its parents. An `@mailwoman/annotations` `Annotator`.
|
|
10
|
+
*/
|
|
11
|
+
import type { Annotator, Nuts } from "@mailwoman/annotations";
|
|
12
|
+
import { DatabaseSync } from "node:sqlite";
|
|
13
|
+
/** Normalized geometry: an array of polygons, each `[outerRing, ...holes]`, each ring
|
|
14
|
+
`[[lon,lat],…]`. */
|
|
15
|
+
export type MultiPolygonCoords = number[][][][];
|
|
16
|
+
/** Inside any polygon of a (multi)polygon feature. */
|
|
17
|
+
export declare function pointInMultiPolygon(lon: number, lat: number, polygons: MultiPolygonCoords): boolean;
|
|
18
|
+
/** Derive the nested NUTS levels from a NUTS id (`"DE111"` → `{ level1:"DE1", level2:"DE11",
|
|
19
|
+
level3:"DE111" }`). */
|
|
20
|
+
export declare function nutsFromId(id: string): Nuts;
|
|
21
|
+
/** A NUTS lookup over a built `node:sqlite` polygon table. */
|
|
22
|
+
export declare class NutsLookup {
|
|
23
|
+
#private;
|
|
24
|
+
constructor(opts: {
|
|
25
|
+
databasePath: string;
|
|
26
|
+
} | {
|
|
27
|
+
database: DatabaseSync;
|
|
28
|
+
});
|
|
29
|
+
/** The nested NUTS codes containing `(lat, lon)`, or null when the point is outside the EU NUTS
|
|
30
|
+
area. */
|
|
31
|
+
find(lat: number, lon: number): Nuts | null;
|
|
32
|
+
close(): void;
|
|
33
|
+
}
|
|
34
|
+
/** Build an `Annotator` filling `AnnotationSet.nuts` for EU coordinates (abstains elsewhere). */
|
|
35
|
+
export declare function makeNutsAnnotator(lookup: NutsLookup): Annotator;
|
|
36
|
+
//# 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,IAAI,EAAE,MAAM,wBAAwB,CAAA;AAC5E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE1C;mBACmB;AACnB,MAAM,MAAM,kBAAkB,GAAG,MAAM,EAAE,EAAE,EAAE,EAAE,CAAA;AAoB/C,sDAAsD;AACtD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAEnG;AAED;sBACsB;AACtB,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAM3C;AAED,8DAA8D;AAC9D,qBAAa,UAAU;;gBAIV,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,QAAQ,EAAE,YAAY,CAAA;KAAE;IAQvE;QACO;IACP,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAU3C,KAAK,IAAI,IAAI;CAGb;AAED,iGAAiG;AACjG,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAK/D"}
|
package/out/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `@mailwoman/nuts-lookup` — EU coordinate → NUTS statistical-region codes (levels 1–3). Point-in-
|
|
7
|
+
* polygon over the Eurostat GISCO NUTS boundaries in a `node:sqlite` table. NUTS ids nest by
|
|
8
|
+
* prefix (`DE` → `DE1` → `DE11` → `DE111`), so we find the deepest containing region and derive
|
|
9
|
+
* its parents. An `@mailwoman/annotations` `Annotator`.
|
|
10
|
+
*/
|
|
11
|
+
import { DatabaseSync } from "node:sqlite";
|
|
12
|
+
function pointInRing(lon, lat, ring) {
|
|
13
|
+
let inside = false;
|
|
14
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
15
|
+
const xi = ring[i][0];
|
|
16
|
+
const yi = ring[i][1];
|
|
17
|
+
const xj = ring[j][0];
|
|
18
|
+
const yj = ring[j][1];
|
|
19
|
+
if (yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi)
|
|
20
|
+
inside = !inside;
|
|
21
|
+
}
|
|
22
|
+
return inside;
|
|
23
|
+
}
|
|
24
|
+
function pointInPolygon(lon, lat, polygon) {
|
|
25
|
+
if (!polygon[0] || !pointInRing(lon, lat, polygon[0]))
|
|
26
|
+
return false;
|
|
27
|
+
for (let i = 1; i < polygon.length; i++)
|
|
28
|
+
if (pointInRing(lon, lat, polygon[i]))
|
|
29
|
+
return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
/** Inside any polygon of a (multi)polygon feature. */
|
|
33
|
+
export function pointInMultiPolygon(lon, lat, polygons) {
|
|
34
|
+
return polygons.some((polygon) => pointInPolygon(lon, lat, polygon));
|
|
35
|
+
}
|
|
36
|
+
/** Derive the nested NUTS levels from a NUTS id (`"DE111"` → `{ level1:"DE1", level2:"DE11",
|
|
37
|
+
level3:"DE111" }`). */
|
|
38
|
+
export function nutsFromId(id) {
|
|
39
|
+
const nuts = {};
|
|
40
|
+
if (id.length >= 3)
|
|
41
|
+
nuts.level1 = id.slice(0, 3);
|
|
42
|
+
if (id.length >= 4)
|
|
43
|
+
nuts.level2 = id.slice(0, 4);
|
|
44
|
+
if (id.length >= 5)
|
|
45
|
+
nuts.level3 = id.slice(0, 5);
|
|
46
|
+
return nuts;
|
|
47
|
+
}
|
|
48
|
+
/** A NUTS lookup over a built `node:sqlite` polygon table. */
|
|
49
|
+
export class NutsLookup {
|
|
50
|
+
#db;
|
|
51
|
+
#byLevelBox;
|
|
52
|
+
constructor(opts) {
|
|
53
|
+
this.#db = "database" in opts ? opts.database : new DatabaseSync(opts.databasePath, { readOnly: true });
|
|
54
|
+
this.#byLevelBox = this.#db.prepare(`SELECT nutsId, geom FROM nuts_regions
|
|
55
|
+
WHERE level = ? AND minLat <= ? AND maxLat >= ? AND minLon <= ? AND maxLon >= ?`);
|
|
56
|
+
}
|
|
57
|
+
/** The nested NUTS codes containing `(lat, lon)`, or null when the point is outside the EU NUTS
|
|
58
|
+
area. */
|
|
59
|
+
find(lat, lon) {
|
|
60
|
+
for (const level of [3, 2, 1]) {
|
|
61
|
+
const rows = this.#byLevelBox.all(level, lat, lat, lon, lon);
|
|
62
|
+
for (const row of rows) {
|
|
63
|
+
if (pointInMultiPolygon(lon, lat, JSON.parse(row.geom)))
|
|
64
|
+
return nutsFromId(row.nutsId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
close() {
|
|
70
|
+
this.#db.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Build an `Annotator` filling `AnnotationSet.nuts` for EU coordinates (abstains elsewhere). */
|
|
74
|
+
export function makeNutsAnnotator(lookup) {
|
|
75
|
+
return ({ lat, lon }) => {
|
|
76
|
+
const nuts = lookup.find(lat, lon);
|
|
77
|
+
return nuts ? { nuts } : {};
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=index.js.map
|
package/out/index.js.map
ADDED
|
@@ -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,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mailwoman/nuts-lookup",
|
|
3
|
+
"version": "4.15.1",
|
|
4
|
+
"description": "EU coordinate → NUTS statistical-region codes (levels 1–3). Point-in-polygon over Eurostat GISCO NUTS boundaries in node:sqlite. 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": "nuts-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
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"out/**/*.js",
|
|
22
|
+
"out/**/*.js.map",
|
|
23
|
+
"out/**/*.d.ts",
|
|
24
|
+
"out/**/*.d.ts.map",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"bin": "./out/cli.js",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|