geodata-kit-tz 0.0.0-stage → 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 geodata-kit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ The code is MIT-licensed. The bundled data keeps the licences of its sources;
24
+ see NOTICE.
package/LICENSE-DATA ADDED
@@ -0,0 +1,27 @@
1
+ Data licence for geodata-kit-tz
2
+ ===============================
3
+
4
+ The file data/timezones.bin is a derived database made from the timezone
5
+ boundaries of timezone-boundary-builder
6
+ (https://github.com/evansiroky/timezone-boundary-builder, release 2026f),
7
+ which are based on OpenStreetMap data.
8
+
9
+ © OpenStreetMap contributors (https://www.openstreetmap.org/copyright)
10
+
11
+ This database is made available under the Open Database License (ODbL) 1.0:
12
+ https://opendatacommons.org/licenses/odbl/1-0/
13
+
14
+ Any rights in individual contents of the database are licensed under the
15
+ Database Contents License: https://opendatacommons.org/licenses/dbcl/1-0/
16
+
17
+ How it was derived: polygons were simplified (Douglas–Peucker, tolerance
18
+ 0.001°), quantised to 1/10,000 degree and packed with a 1°×1° grid index by
19
+ scripts/build/tz.ts in the geodata-kit repository, which is the means of
20
+ reproducing it from the source.
21
+
22
+ ODbL is a share-alike licence: if you publicly use an adapted version of this
23
+ database, you must offer that adapted database under the ODbL as well. Using
24
+ the lookup results in your application (a "Produced Work") requires
25
+ attribution, for example: "Timezone data © OpenStreetMap contributors (ODbL)".
26
+
27
+ The JavaScript code of geodata-kit-tz is MIT-licensed (see LICENSE).
package/NOTICE ADDED
@@ -0,0 +1,91 @@
1
+ geodata-kit — third-party data notices
2
+ ======================================
3
+
4
+ The code of geodata-kit is MIT-licensed (see LICENSE). The data shipped in
5
+ the packages was generated from the sources below and keeps their licences.
6
+ Every source, its download URL, size, SHA-256 and download date is listed in
7
+ data/meta.json of the repository.
8
+
9
+ GeoNames
10
+ Countries, subdivisions, cities, localities, alternate names, airports,
11
+ timezones per place and postal codes. Data from GeoNames (https://www.geonames.org),
12
+ licensed under Creative Commons Attribution 4.0 (CC BY 4.0,
13
+ https://creativecommons.org/licenses/by/4.0/). Modified: filtered,
14
+ re-keyed and combined with the other sources listed here.
15
+
16
+ Unicode CLDR
17
+ Country, language and subdivision names, currencies with dates, retired
18
+ codes, UN M49 regions, languages per country, likely locales, week and
19
+ measurement data. Copyright © 1991-2025 Unicode, Inc. Licensed under the
20
+ Unicode License v3 (https://www.unicode.org/license.txt):
21
+
22
+ UNICODE LICENSE V3
23
+ COPYRIGHT AND PERMISSION NOTICE
24
+ Copyright © 1991-2025 Unicode, Inc.
25
+ NOTICE TO USER: Carefully read the following legal agreement. BY
26
+ DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
27
+ SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
28
+ TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
29
+ DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
30
+ Permission is hereby granted, free of charge, to any person obtaining a
31
+ copy of data files and any associated documentation (the "Data Files") or
32
+ software and any associated documentation (the "Software") to deal in the
33
+ Data Files or Software without restriction, including without limitation
34
+ the rights to use, copy, modify, merge, publish, distribute, and/or sell
35
+ copies of the Data Files or Software, and to permit persons to whom the
36
+ Data Files or Software are furnished to do so, provided that either (a)
37
+ this copyright and permission notice appear with all copies of the Data
38
+ Files or Software, or (b) this copyright and permission notice appear in
39
+ associated Documentation.
40
+ THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
41
+ KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
42
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
43
+ THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
44
+ INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
45
+ OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
46
+ USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
47
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
48
+ PERFORMANCE OF THE DATA FILES OR SOFTWARE.
49
+ Except as contained in this notice, the name of a copyright holder shall
50
+ not be used in advertising or otherwise to promote the sale, use or other
51
+ dealings in these Data Files or Software without prior written
52
+ authorization of the copyright holder.
53
+
54
+ IANA Time Zone Database
55
+ Timezone names, zones per country and legacy aliases, from the IANA Time
56
+ Zone Database (https://www.iana.org/time-zones), which is in the public domain.
57
+
58
+ Wikidata
59
+ Subdivision ISO 3166-2 ↔ GeoNames mapping, official languages of countries
60
+ and subdivisions, demonyms, calling codes, driving side. From Wikidata
61
+ (https://www.wikidata.org), dedicated to the public domain under CC0 1.0.
62
+
63
+ Natural Earth
64
+ Country and first-level subdivision polygons. Made with Natural Earth.
65
+ Free vector and raster map data @ naturalearthdata.com (public domain).
66
+
67
+ libphonenumber
68
+ Phone-number metadata, converted from PhoneNumberMetadata.xml.
69
+ Copyright (C) The Libphonenumber Authors. Licensed under the Apache
70
+ License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0).
71
+ You may not use this data except in compliance with that License. Unless
72
+ required by applicable law or agreed to in writing, it is distributed on
73
+ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
74
+
75
+ Google address metadata (libaddressinput)
76
+ Address formats, required fields, postal-code patterns and region lists.
77
+ Data from Google's libaddressinput project
78
+ (https://github.com/google/libaddressinput), licensed under CC BY 4.0.
79
+ Modified: merged with GeoNames postal formats.
80
+
81
+ European Union / EFTA
82
+ EU, Eurozone, Schengen, EEA and EFTA membership dates, compiled from
83
+ europa.eu (© European Union, CC BY 4.0) and efta.int. Modified.
84
+
85
+ geodata-kit-tz only
86
+ Timezone polygons from timezone-boundary-builder
87
+ (https://github.com/evansiroky/timezone-boundary-builder), based on data
88
+ © OpenStreetMap contributors, licensed under the Open Database License
89
+ (ODbL) 1.0 (https://opendatacommons.org/licenses/odbl/1-0/). The ODbL
90
+ applies to that data, not to the rest of geodata-kit. See LICENSE-DATA in
91
+ that package.
package/README.md CHANGED
@@ -1,3 +1,26 @@
1
- # Temporary Holding Version
1
+ # geodata-kit-tz
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The exact timezone for any coordinate. Works in Node.js and browsers.
4
+
5
+ ```bash
6
+ npm install geodata-kit-tz
7
+ ```
8
+
9
+ ```ts
10
+ import { getTimezoneAt } from 'geodata-kit-tz';
11
+
12
+ await getTimezoneAt(39.77, -86.16); // 'America/Indiana/Indianapolis'
13
+ await getTimezoneAt(0, -30); // 'Etc/GMT+2' (at sea)
14
+ ```
15
+
16
+ ## API
17
+
18
+ | Function | Returns |
19
+ |---|---|
20
+ | `getTimezoneAt(lat, lng)` | async; IANA zone, or `Etc/GMT±N` at sea |
21
+ | `getTimezonesAt(lat, lng)` | async; all zones at the point (a few areas overlap), most specific first |
22
+ | `listPolygonTimezones()` | async; every zone in the data |
23
+ | `nauticalTimezone(lng)` | `Etc/GMT±N` for a longitude |
24
+
25
+ Code: MIT. Data: © OpenStreetMap contributors, Open Database License (ODbL);
26
+ see `LICENSE-DATA`. Credit OpenStreetMap when you show results.
Binary file
package/dist/index.cjs ADDED
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ getTimezoneAt: () => getTimezoneAt,
34
+ getTimezonesAt: () => getTimezonesAt,
35
+ listPolygonTimezones: () => listPolygonTimezones,
36
+ nauticalTimezone: () => nauticalTimezone
37
+ });
38
+ module.exports = __toCommonJS(src_exports);
39
+
40
+ // ../../node_modules/tsup/assets/cjs_shims.js
41
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
42
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
43
+
44
+ // src/index.ts
45
+ var DATA_URL = new URL("../data/timezones.bin", importMetaUrl);
46
+ var SCALE = 1e4;
47
+ var indexPromise = null;
48
+ async function readData() {
49
+ const isNode = typeof process !== "undefined" && Boolean(process.versions?.node);
50
+ if (isNode && DATA_URL.protocol === "file:") {
51
+ const { readFile } = await import("fs/promises");
52
+ const file = await readFile(DATA_URL);
53
+ return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
54
+ }
55
+ const response = await fetch(DATA_URL);
56
+ if (!response.ok) throw new Error(`geodata-kit-tz: cannot load ${DATA_URL.href} (HTTP ${response.status})`);
57
+ return response.arrayBuffer();
58
+ }
59
+ function parse(buffer) {
60
+ const view = new DataView(buffer);
61
+ const decoder = new TextDecoder();
62
+ if (decoder.decode(new Uint8Array(buffer, 0, 4)) !== "GKTZ") throw new Error("geodata-kit-tz: not a timezone index");
63
+ let offset = 8;
64
+ const zoneCount = view.getUint32(offset, true);
65
+ offset += 4;
66
+ const zones = [];
67
+ for (let i = 0; i < zoneCount; i++) {
68
+ const length = view.getUint16(offset, true);
69
+ offset += 2;
70
+ zones.push(decoder.decode(new Uint8Array(buffer, offset, length)));
71
+ offset += length;
72
+ }
73
+ const polygonCount = view.getUint32(offset, true);
74
+ offset += 4;
75
+ const polygons = [];
76
+ for (let i = 0; i < polygonCount; i++) {
77
+ const zone = view.getUint16(offset, true);
78
+ offset += 2;
79
+ const ringCount = view.getUint16(offset, true);
80
+ offset += 2;
81
+ const bbox = [
82
+ view.getInt32(offset, true),
83
+ view.getInt32(offset + 4, true),
84
+ view.getInt32(offset + 8, true),
85
+ view.getInt32(offset + 12, true)
86
+ ];
87
+ offset += 16;
88
+ const rings = [];
89
+ for (let r = 0; r < ringCount; r++) {
90
+ const pointCount = view.getUint32(offset, true);
91
+ offset += 4;
92
+ rings.push(new Int32Array(buffer.slice(offset, offset + pointCount * 8)));
93
+ offset += pointCount * 8;
94
+ }
95
+ polygons.push({ zone, bbox, rings });
96
+ }
97
+ const cellOffsets = new Uint32Array(buffer.slice(offset, offset + (360 * 180 + 1) * 4));
98
+ offset += (360 * 180 + 1) * 4;
99
+ const cellList = new Uint32Array(buffer.slice(offset));
100
+ return { zones, polygons, cellOffsets, cellList };
101
+ }
102
+ function load() {
103
+ indexPromise ?? (indexPromise = readData().then(parse));
104
+ return indexPromise;
105
+ }
106
+ function insideRing(ring, x, y) {
107
+ let inside = false;
108
+ for (let i = 0, j = ring.length - 2; i < ring.length; j = i, i += 2) {
109
+ const xi = ring[i];
110
+ const yi = ring[i + 1];
111
+ const xj = ring[j];
112
+ const yj = ring[j + 1];
113
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
114
+ }
115
+ return inside;
116
+ }
117
+ function nauticalTimezone(lng) {
118
+ const offset = Math.round(lng / 15);
119
+ if (offset === 0) return "Etc/GMT";
120
+ return `Etc/GMT${offset > 0 ? "-" : "+"}${Math.abs(offset)}`;
121
+ }
122
+ async function zonesAt(lat, lng, firstOnly) {
123
+ if (!Number.isFinite(lat) || !Number.isFinite(lng) || lat < -90 || lat > 90) throw new RangeError(`Invalid coordinate ${lat}, ${lng}`);
124
+ const normalizedLng = ((lng + 180) % 360 + 360) % 360 - 180;
125
+ const index = await load();
126
+ const x = Math.round(normalizedLng * SCALE);
127
+ const y = Math.round(lat * SCALE);
128
+ const row = Math.min(179, Math.max(0, Math.floor(lat) + 90));
129
+ const col = Math.min(359, Math.max(0, Math.floor(normalizedLng) + 180));
130
+ const cell = row * 360 + col;
131
+ const found = [];
132
+ for (let i = index.cellOffsets[cell]; i < index.cellOffsets[cell + 1]; i++) {
133
+ const polygon = index.polygons[index.cellList[i]];
134
+ const [minX, minY, maxX, maxY] = polygon.bbox;
135
+ if (x < minX || x > maxX || y < minY || y > maxY) continue;
136
+ if (insideRing(polygon.rings[0], x, y) && !polygon.rings.slice(1).some((hole) => insideRing(hole, x, y))) {
137
+ const zone = index.zones[polygon.zone];
138
+ if (!found.includes(zone)) found.push(zone);
139
+ if (firstOnly) break;
140
+ }
141
+ }
142
+ return found.length ? found : [nauticalTimezone(normalizedLng)];
143
+ }
144
+ async function getTimezoneAt(lat, lng) {
145
+ return (await zonesAt(lat, lng, true))[0];
146
+ }
147
+ async function getTimezonesAt(lat, lng) {
148
+ return zonesAt(lat, lng, false);
149
+ }
150
+ async function listPolygonTimezones() {
151
+ return [...(await load()).zones];
152
+ }
153
+ // Annotate the CommonJS export names for ESM import in node:
154
+ 0 && (module.exports = {
155
+ getTimezoneAt,
156
+ getTimezonesAt,
157
+ listPolygonTimezones,
158
+ nauticalTimezone
159
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * geodata-kit-tz — exact IANA timezone for a coordinate, from timezone
3
+ * polygons (timezone-boundary-builder, data © OpenStreetMap contributors,
4
+ * ODbL 1.0 — see LICENSE-DATA). Polygons are simplified to about 110 m.
5
+ *
6
+ * Works in Node.js (reads the file) and in browsers (fetches it next to the module).
7
+ */
8
+ /** Nautical timezone for open sea: Etc/GMT-1 east of 7.5°E, etc. (the sign is inverted in these names). */
9
+ declare function nauticalTimezone(lng: number): string;
10
+ /**
11
+ * IANA timezone at a coordinate: (48.137, 11.575) → "Europe/Berlin",
12
+ * (39.77, -86.16) → "America/Indiana/Indianapolis". At sea: "Etc/GMT±N".
13
+ * Where zones overlap in the source (Xinjiang: Asia/Urumqi inside Asia/Shanghai),
14
+ * the most specific (smallest) one is returned; see getTimezonesAt.
15
+ */
16
+ declare function getTimezoneAt(lat: number, lng: number): Promise<string>;
17
+ /** Every zone whose polygon contains the point, most specific first (usually one). */
18
+ declare function getTimezonesAt(lat: number, lng: number): Promise<string[]>;
19
+ /** Every timezone name in the polygon data. */
20
+ declare function listPolygonTimezones(): Promise<string[]>;
21
+
22
+ export { getTimezoneAt, getTimezonesAt, listPolygonTimezones, nauticalTimezone };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * geodata-kit-tz — exact IANA timezone for a coordinate, from timezone
3
+ * polygons (timezone-boundary-builder, data © OpenStreetMap contributors,
4
+ * ODbL 1.0 — see LICENSE-DATA). Polygons are simplified to about 110 m.
5
+ *
6
+ * Works in Node.js (reads the file) and in browsers (fetches it next to the module).
7
+ */
8
+ /** Nautical timezone for open sea: Etc/GMT-1 east of 7.5°E, etc. (the sign is inverted in these names). */
9
+ declare function nauticalTimezone(lng: number): string;
10
+ /**
11
+ * IANA timezone at a coordinate: (48.137, 11.575) → "Europe/Berlin",
12
+ * (39.77, -86.16) → "America/Indiana/Indianapolis". At sea: "Etc/GMT±N".
13
+ * Where zones overlap in the source (Xinjiang: Asia/Urumqi inside Asia/Shanghai),
14
+ * the most specific (smallest) one is returned; see getTimezonesAt.
15
+ */
16
+ declare function getTimezoneAt(lat: number, lng: number): Promise<string>;
17
+ /** Every zone whose polygon contains the point, most specific first (usually one). */
18
+ declare function getTimezonesAt(lat: number, lng: number): Promise<string[]>;
19
+ /** Every timezone name in the polygon data. */
20
+ declare function listPolygonTimezones(): Promise<string[]>;
21
+
22
+ export { getTimezoneAt, getTimezonesAt, listPolygonTimezones, nauticalTimezone };
package/dist/index.js ADDED
@@ -0,0 +1,115 @@
1
+ // src/index.ts
2
+ var DATA_URL = new URL("../data/timezones.bin", import.meta.url);
3
+ var SCALE = 1e4;
4
+ var indexPromise = null;
5
+ async function readData() {
6
+ const isNode = typeof process !== "undefined" && Boolean(process.versions?.node);
7
+ if (isNode && DATA_URL.protocol === "file:") {
8
+ const { readFile } = await import("fs/promises");
9
+ const file = await readFile(DATA_URL);
10
+ return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
11
+ }
12
+ const response = await fetch(DATA_URL);
13
+ if (!response.ok) throw new Error(`geodata-kit-tz: cannot load ${DATA_URL.href} (HTTP ${response.status})`);
14
+ return response.arrayBuffer();
15
+ }
16
+ function parse(buffer) {
17
+ const view = new DataView(buffer);
18
+ const decoder = new TextDecoder();
19
+ if (decoder.decode(new Uint8Array(buffer, 0, 4)) !== "GKTZ") throw new Error("geodata-kit-tz: not a timezone index");
20
+ let offset = 8;
21
+ const zoneCount = view.getUint32(offset, true);
22
+ offset += 4;
23
+ const zones = [];
24
+ for (let i = 0; i < zoneCount; i++) {
25
+ const length = view.getUint16(offset, true);
26
+ offset += 2;
27
+ zones.push(decoder.decode(new Uint8Array(buffer, offset, length)));
28
+ offset += length;
29
+ }
30
+ const polygonCount = view.getUint32(offset, true);
31
+ offset += 4;
32
+ const polygons = [];
33
+ for (let i = 0; i < polygonCount; i++) {
34
+ const zone = view.getUint16(offset, true);
35
+ offset += 2;
36
+ const ringCount = view.getUint16(offset, true);
37
+ offset += 2;
38
+ const bbox = [
39
+ view.getInt32(offset, true),
40
+ view.getInt32(offset + 4, true),
41
+ view.getInt32(offset + 8, true),
42
+ view.getInt32(offset + 12, true)
43
+ ];
44
+ offset += 16;
45
+ const rings = [];
46
+ for (let r = 0; r < ringCount; r++) {
47
+ const pointCount = view.getUint32(offset, true);
48
+ offset += 4;
49
+ rings.push(new Int32Array(buffer.slice(offset, offset + pointCount * 8)));
50
+ offset += pointCount * 8;
51
+ }
52
+ polygons.push({ zone, bbox, rings });
53
+ }
54
+ const cellOffsets = new Uint32Array(buffer.slice(offset, offset + (360 * 180 + 1) * 4));
55
+ offset += (360 * 180 + 1) * 4;
56
+ const cellList = new Uint32Array(buffer.slice(offset));
57
+ return { zones, polygons, cellOffsets, cellList };
58
+ }
59
+ function load() {
60
+ indexPromise ?? (indexPromise = readData().then(parse));
61
+ return indexPromise;
62
+ }
63
+ function insideRing(ring, x, y) {
64
+ let inside = false;
65
+ for (let i = 0, j = ring.length - 2; i < ring.length; j = i, i += 2) {
66
+ const xi = ring[i];
67
+ const yi = ring[i + 1];
68
+ const xj = ring[j];
69
+ const yj = ring[j + 1];
70
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
71
+ }
72
+ return inside;
73
+ }
74
+ function nauticalTimezone(lng) {
75
+ const offset = Math.round(lng / 15);
76
+ if (offset === 0) return "Etc/GMT";
77
+ return `Etc/GMT${offset > 0 ? "-" : "+"}${Math.abs(offset)}`;
78
+ }
79
+ async function zonesAt(lat, lng, firstOnly) {
80
+ if (!Number.isFinite(lat) || !Number.isFinite(lng) || lat < -90 || lat > 90) throw new RangeError(`Invalid coordinate ${lat}, ${lng}`);
81
+ const normalizedLng = ((lng + 180) % 360 + 360) % 360 - 180;
82
+ const index = await load();
83
+ const x = Math.round(normalizedLng * SCALE);
84
+ const y = Math.round(lat * SCALE);
85
+ const row = Math.min(179, Math.max(0, Math.floor(lat) + 90));
86
+ const col = Math.min(359, Math.max(0, Math.floor(normalizedLng) + 180));
87
+ const cell = row * 360 + col;
88
+ const found = [];
89
+ for (let i = index.cellOffsets[cell]; i < index.cellOffsets[cell + 1]; i++) {
90
+ const polygon = index.polygons[index.cellList[i]];
91
+ const [minX, minY, maxX, maxY] = polygon.bbox;
92
+ if (x < minX || x > maxX || y < minY || y > maxY) continue;
93
+ if (insideRing(polygon.rings[0], x, y) && !polygon.rings.slice(1).some((hole) => insideRing(hole, x, y))) {
94
+ const zone = index.zones[polygon.zone];
95
+ if (!found.includes(zone)) found.push(zone);
96
+ if (firstOnly) break;
97
+ }
98
+ }
99
+ return found.length ? found : [nauticalTimezone(normalizedLng)];
100
+ }
101
+ async function getTimezoneAt(lat, lng) {
102
+ return (await zonesAt(lat, lng, true))[0];
103
+ }
104
+ async function getTimezonesAt(lat, lng) {
105
+ return zonesAt(lat, lng, false);
106
+ }
107
+ async function listPolygonTimezones() {
108
+ return [...(await load()).zones];
109
+ }
110
+ export {
111
+ getTimezoneAt,
112
+ getTimezonesAt,
113
+ listPolygonTimezones,
114
+ nauticalTimezone
115
+ };
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "geodata-kit-tz",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Optional companion to geodata-kit: exact IANA timezone for any coordinate from timezone polygons (timezone-boundary-builder). Code MIT, data ODbL 1.0.",
5
+ "keywords": [
6
+ "timezone",
7
+ "tz",
8
+ "iana",
9
+ "coordinates",
10
+ "point-in-polygon",
11
+ "geodata-kit"
12
+ ],
13
+ "license": "MIT AND ODbL-1.0",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/greta-devhub/geodata-kit.git",
17
+ "directory": "packages/geodata-kit-tz"
18
+ },
19
+ "homepage": "https://github.com/greta-devhub/geodata-kit/tree/main/packages/geodata-kit-tz#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/greta-devhub/geodata-kit/issues"
22
+ },
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "import": {
34
+ "types": "./dist/index.d.ts",
35
+ "default": "./dist/index.js"
36
+ },
37
+ "require": {
38
+ "types": "./dist/index.d.cts",
39
+ "default": "./dist/index.cjs"
40
+ }
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "data",
47
+ "NOTICE",
48
+ "LICENSE",
49
+ "LICENSE-DATA",
50
+ "README.md"
51
+ ],
52
+ "scripts": {
53
+ "build": "tsup"
54
+ }
55
+ }