@mailwoman/timezone-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,35 @@
1
+ # @mailwoman/timezone-lookup
2
+
3
+ Coordinate → IANA timezone, server-side. Point-in-polygon over
4
+ [timezone-boundary-builder](https://github.com/evansiroky/timezone-boundary-builder) polygons in a
5
+ `node:sqlite` DB; the UTC offset comes from `Intl` (no tz-database dependency). An
6
+ [`@mailwoman/annotations`](../annotations) `Annotator`.
7
+
8
+ ## Build the DB
9
+
10
+ ```bash
11
+ # from the downloaded combined-with-oceans.json (tz-boundary-builder release)
12
+ npx @mailwoman/timezone-lookup build --geojson combined-with-oceans.json --out timezone.db
13
+ ```
14
+
15
+ ## Look up
16
+
17
+ ```bash
18
+ npx @mailwoman/timezone-lookup --db timezone.db 40.7128 -74.0060
19
+ # {"timezone":"America/New_York","offsetSec":-18000}
20
+ ```
21
+
22
+ ## Library
23
+
24
+ ```ts
25
+ import { TimezoneLookup, makeTimezoneAnnotator } from "@mailwoman/timezone-lookup"
26
+
27
+ const lookup = new TimezoneLookup({ databasePath: "timezone.db" })
28
+ lookup.find(35.6762, 139.6503) // "Asia/Tokyo"
29
+
30
+ const annotator = makeTimezoneAnnotator(lookup) // fills AnnotationSet.timezone
31
+ ```
32
+
33
+ Server-side only — the polygon PIP runs over `node:sqlite`. A browser/WASM build is a follow-up.
34
+
35
+ Data: timezone-boundary-builder (ODbL). Attribution + share-alike apply to the built DB.
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 timezone polygon DB from a timezone-boundary-builder GeoJSON
7
+ * (`combined-with-oceans.json`, features of `{ properties: { tzid }, geometry:
8
+ * Polygon|MultiPolygon }`). One row per feature: the tzid, a bounding box (for the lookup's
9
+ * prefilter), and the geometry normalized to MultiPolygon coordinates as JSON.
10
+ */
11
+ /** Read the GeoJSON at `geojsonPath` and write the polygon DB to `dbPath` (overwriting its table). */
12
+ export declare function buildTimezoneDb(geojsonPath: string, dbPath: string): {
13
+ features: 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;;;;;;;;;GASG;AAWH,sGAAsG;AACtG,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAwCzF"}
package/out/build.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Build the timezone polygon DB from a timezone-boundary-builder GeoJSON
7
+ * (`combined-with-oceans.json`, features of `{ properties: { tzid }, geometry:
8
+ * Polygon|MultiPolygon }`). One row per feature: the tzid, a bounding box (for the lookup's
9
+ * prefilter), and the geometry normalized to MultiPolygon coordinates as JSON.
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+ import { DatabaseSync } from "node:sqlite";
13
+ /** Read the GeoJSON at `geojsonPath` and write the polygon DB to `dbPath` (overwriting its table). */
14
+ export function buildTimezoneDb(geojsonPath, dbPath) {
15
+ const data = JSON.parse(readFileSync(geojsonPath, "utf8"));
16
+ const db = new DatabaseSync(dbPath);
17
+ db.exec("DROP TABLE IF EXISTS timezone_polygons");
18
+ db.exec(`CREATE TABLE timezone_polygons (tzid TEXT NOT NULL, minLat REAL, maxLat REAL, minLon REAL, maxLon REAL, geom TEXT NOT NULL)`);
19
+ const insert = db.prepare("INSERT INTO timezone_polygons (tzid, minLat, maxLat, minLon, maxLon, geom) VALUES (?,?,?,?,?,?)");
20
+ db.exec("BEGIN");
21
+ for (const feature of data.features) {
22
+ const polygons = feature.geometry.type === "Polygon"
23
+ ? [feature.geometry.coordinates]
24
+ : feature.geometry.coordinates;
25
+ let minLat = 90;
26
+ let maxLat = -90;
27
+ let minLon = 180;
28
+ let maxLon = -180;
29
+ for (const polygon of polygons) {
30
+ for (const ring of polygon) {
31
+ for (const point of ring) {
32
+ const lon = point[0];
33
+ const lat = point[1];
34
+ if (lat < minLat)
35
+ minLat = lat;
36
+ if (lat > maxLat)
37
+ maxLat = lat;
38
+ if (lon < minLon)
39
+ minLon = lon;
40
+ if (lon > maxLon)
41
+ maxLon = lon;
42
+ }
43
+ }
44
+ }
45
+ insert.run(feature.properties.tzid, minLat, maxLat, minLon, maxLon, JSON.stringify(polygons));
46
+ }
47
+ db.exec("COMMIT");
48
+ db.exec("CREATE INDEX idx_tz_bbox ON timezone_polygons (minLat, maxLat, minLon, maxLon)");
49
+ db.close();
50
+ return { features: data.features.length };
51
+ }
52
+ //# sourceMappingURL=build.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../build.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAQ1C,sGAAsG;AACtG,MAAM,UAAU,eAAe,CAAC,WAAmB,EAAE,MAAc;IAClE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAoC,CAAA;IAC7F,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAA;IACnC,EAAE,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAA;IACjD,EAAE,CAAC,IAAI,CACN,6HAA6H,CAC7H,CAAA;IACD,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CACxB,iGAAiG,CACjG,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;QAEpD,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,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC9F,CAAC;IACD,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACjB,EAAE,CAAC,IAAI,CAAC,gFAAgF,CAAC,CAAA;IACzF,EAAE,CAAC,KAAK,EAAE,CAAA;IACV,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAA;AAC1C,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-timezone` — build the polygon DB, or look up a coordinate's IANA timezone.
8
+ *
9
+ * Mailwoman-timezone build --geojson combined-with-oceans.json --out timezone.db mailwoman-timezone
10
+ * --db timezone.db 40.7128 -74.0060
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=cli.d.ts.map
@@ -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,52 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @copyright Sister Software
4
+ * @license AGPL-3.0
5
+ * @author Teffen Ellis, et al.
6
+ *
7
+ * `mailwoman-timezone` — build the polygon DB, or look up a coordinate's IANA timezone.
8
+ *
9
+ * Mailwoman-timezone build --geojson combined-with-oceans.json --out timezone.db mailwoman-timezone
10
+ * --db timezone.db 40.7128 -74.0060
11
+ */
12
+ import { parseArgs } from "node:util";
13
+ import { buildTimezoneDb } from "./build.js";
14
+ import { offsetSecForTimezone, TimezoneLookup } 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-timezone build --geojson <path> --out <db>");
22
+ process.exit(1);
23
+ }
24
+ const { features } = buildTimezoneDb(values.geojson, values.out);
25
+ console.error(`built ${values.out} (${features} features)`);
26
+ }
27
+ else {
28
+ // Hand-parse so negative longitudes (which look like options to parseArgs) 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-timezone --db <db> <lat> <lon>");
45
+ process.exit(1);
46
+ }
47
+ const lookup = new TimezoneLookup({ databasePath });
48
+ const tzid = lookup.find(lat, lon);
49
+ console.log(JSON.stringify({ timezone: tzid, offsetSec: tzid ? offsetSecForTimezone(tzid) : null }));
50
+ lookup.close();
51
+ }
52
+ //# 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,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEjE,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,6DAA6D,CAAC,CAAA;QAC5E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,GAAG,eAAe,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;IAChE,OAAO,CAAC,KAAK,CAAC,SAAS,MAAM,CAAC,GAAG,KAAK,QAAQ,YAAY,CAAC,CAAA;AAC5D,CAAC;KAAM,CAAC;IACP,gGAAgG;IAChG,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,iDAAiD,CAAC,CAAA;QAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC,CAAA;IACnD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAClC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;IACpG,MAAM,CAAC,KAAK,EAAE,CAAA;AACf,CAAC"}
package/out/index.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/timezone-lookup` — coordinate → IANA timezone, server-side. Point-in-polygon over the
7
+ * timezone-boundary-builder polygons stored in a `node:sqlite` DB (bbox-prefilter + ray-cast),
8
+ * mirroring the resolver's PIP pattern. The UTC offset comes from `Intl.DateTimeFormat` — no tz
9
+ * database dependency. Build the DB with `mailwoman-timezone build` (see `./build.ts`).
10
+ */
11
+ import type { Annotator } 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
+ /**
19
+ * The current UTC offset (seconds) for an IANA timezone, via `Intl` (no tz-db dependency). Returns
20
+ * `undefined` if the runtime can't resolve the zone.
21
+ */
22
+ export declare function offsetSecForTimezone(tzid: string, date?: Date): number | undefined;
23
+ /** A timezone lookup over a built `node:sqlite` polygon DB. */
24
+ export declare class TimezoneLookup {
25
+ #private;
26
+ constructor(opts: {
27
+ databasePath: string;
28
+ } | {
29
+ database: DatabaseSync;
30
+ });
31
+ /** The IANA timezone id containing `(lat, lon)`, or `null` if none (shouldn't happen with oceans). */
32
+ find(lat: number, lon: number): string | null;
33
+ close(): void;
34
+ }
35
+ /** Build an `Annotator` that fills `AnnotationSet.timezone` (name + current offset) from a lookup. */
36
+ export declare function makeTimezoneAnnotator(lookup: TimezoneLookup): Annotator;
37
+ //# 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;mBACmB;AACnB,MAAM,MAAM,kBAAkB,GAAG,MAAM,EAAE,EAAE,EAAE,EAAE,CAAA;AAsB/C,sDAAsD;AACtD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAEnG;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,IAAiB,GAAG,MAAM,GAAG,SAAS,CAa9F;AAED,+DAA+D;AAC/D,qBAAa,cAAc;;gBAId,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,QAAQ,EAAE,YAAY,CAAA;KAAE;IASvE,sGAAsG;IACtG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAQ7C,KAAK,IAAI,IAAI;CAGb;AAED,sGAAsG;AACtG,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,CAOvE"}
package/out/index.js ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/timezone-lookup` — coordinate → IANA timezone, server-side. Point-in-polygon over the
7
+ * timezone-boundary-builder polygons stored in a `node:sqlite` DB (bbox-prefilter + ray-cast),
8
+ * mirroring the resolver's PIP pattern. The UTC offset comes from `Intl.DateTimeFormat` — no tz
9
+ * database dependency. Build the DB with `mailwoman-timezone build` (see `./build.ts`).
10
+ */
11
+ import { DatabaseSync } from "node:sqlite";
12
+ /** Ray-cast point-in-ring (even-odd rule). `ring` is `[[lon, lat], …]`. */
13
+ function pointInRing(lon, lat, ring) {
14
+ let inside = false;
15
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
16
+ const xi = ring[i][0];
17
+ const yi = ring[i][1];
18
+ const xj = ring[j][0];
19
+ const yj = ring[j][1];
20
+ if (yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi)
21
+ inside = !inside;
22
+ }
23
+ return inside;
24
+ }
25
+ /** Inside the outer ring and outside every hole. */
26
+ function pointInPolygon(lon, lat, polygon) {
27
+ if (!polygon[0] || !pointInRing(lon, lat, polygon[0]))
28
+ return false;
29
+ for (let i = 1; i < polygon.length; i++)
30
+ if (pointInRing(lon, lat, polygon[i]))
31
+ return false;
32
+ return true;
33
+ }
34
+ /** Inside any polygon of a (multi)polygon feature. */
35
+ export function pointInMultiPolygon(lon, lat, polygons) {
36
+ return polygons.some((polygon) => pointInPolygon(lon, lat, polygon));
37
+ }
38
+ /**
39
+ * The current UTC offset (seconds) for an IANA timezone, via `Intl` (no tz-db dependency). Returns
40
+ * `undefined` if the runtime can't resolve the zone.
41
+ */
42
+ export function offsetSecForTimezone(tzid, date = new Date()) {
43
+ try {
44
+ const parts = new Intl.DateTimeFormat("en-US", { timeZone: tzid, timeZoneName: "longOffset" }).formatToParts(date);
45
+ const name = parts.find((p) => p.type === "timeZoneName")?.value ?? "";
46
+ const match = name.match(/GMT([+-])(\d{2}):?(\d{2})?/);
47
+ if (!match)
48
+ return name === "GMT" ? 0 : undefined;
49
+ const sign = match[1] === "-" ? -1 : 1;
50
+ const hours = Number(match[2]);
51
+ const minutes = Number(match[3] ?? "0");
52
+ return sign * (hours * 3600 + minutes * 60);
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ /** A timezone lookup over a built `node:sqlite` polygon DB. */
59
+ export class TimezoneLookup {
60
+ #db;
61
+ #stmt;
62
+ constructor(opts) {
63
+ this.#db = "database" in opts ? opts.database : new DatabaseSync(opts.databasePath, { readOnly: true });
64
+ // Candidate features whose bbox contains the point; PIP picks the exact one.
65
+ this.#stmt = this.#db.prepare(`SELECT tzid, geom FROM timezone_polygons
66
+ WHERE minLat <= ? AND maxLat >= ? AND minLon <= ? AND maxLon >= ?`);
67
+ }
68
+ /** The IANA timezone id containing `(lat, lon)`, or `null` if none (shouldn't happen with oceans). */
69
+ find(lat, lon) {
70
+ const rows = this.#stmt.all(lat, lat, lon, lon);
71
+ for (const row of rows) {
72
+ if (pointInMultiPolygon(lon, lat, JSON.parse(row.geom)))
73
+ return row.tzid;
74
+ }
75
+ return null;
76
+ }
77
+ close() {
78
+ this.#db.close();
79
+ }
80
+ }
81
+ /** Build an `Annotator` that fills `AnnotationSet.timezone` (name + current offset) from a lookup. */
82
+ export function makeTimezoneAnnotator(lookup) {
83
+ return ({ lat, lon, date }) => {
84
+ const name = lookup.find(lat, lon);
85
+ if (!name)
86
+ return {};
87
+ const offsetSec = offsetSecForTimezone(name, date);
88
+ return { timezone: offsetSec != null ? { name, offsetSec } : { name } };
89
+ };
90
+ }
91
+ //# 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,31 @@
1
+ {
2
+ "name": "@mailwoman/timezone-lookup",
3
+ "version": "4.15.1",
4
+ "description": "Coordinate → IANA timezone, server-side. Point-in-polygon over timezone-boundary-builder polygons in a node:sqlite DB; UTC offset via Intl (no tz-db dependency). 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": "timezone-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
+ }