@zukall/zap 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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/PROTOCOL-V1.md +117 -0
  3. package/PROTOCOL.md +176 -0
  4. package/README.md +418 -2
  5. package/THIRD-PARTY-NOTICES.md +19 -0
  6. package/dist/cjs/address.d.ts +3 -0
  7. package/dist/cjs/address.js +38 -0
  8. package/dist/cjs/address_bounty.d.ts +26 -0
  9. package/dist/cjs/address_bounty.js +2334 -0
  10. package/dist/cjs/address_province_bounty.d.ts +56 -0
  11. package/dist/cjs/address_province_bounty.js +5563 -0
  12. package/dist/cjs/country.d.ts +6 -0
  13. package/dist/cjs/country.js +27 -0
  14. package/dist/cjs/data.d.ts +3 -0
  15. package/dist/cjs/data.js +33 -0
  16. package/dist/cjs/directions.d.ts +5 -0
  17. package/dist/cjs/directions.js +13 -0
  18. package/dist/cjs/encoding.d.ts +3 -0
  19. package/dist/cjs/encoding.js +20 -0
  20. package/dist/cjs/grid.d.ts +23 -0
  21. package/dist/cjs/grid.js +152 -0
  22. package/dist/cjs/index.d.ts +27 -0
  23. package/dist/cjs/index.js +26 -0
  24. package/dist/cjs/internal.d.ts +3 -0
  25. package/dist/cjs/internal.js +19 -0
  26. package/dist/cjs/legacy-address.d.ts +3 -0
  27. package/dist/cjs/legacy-address.js +44 -0
  28. package/dist/cjs/package.json +1 -0
  29. package/dist/cjs/protocol.d.ts +22 -0
  30. package/dist/cjs/protocol.js +25 -0
  31. package/dist/cjs/types.d.ts +114 -0
  32. package/dist/cjs/types.js +2 -0
  33. package/dist/esm/address.d.ts +3 -0
  34. package/dist/esm/address.js +35 -0
  35. package/dist/esm/address_bounty.d.ts +26 -0
  36. package/dist/esm/address_bounty.js +2331 -0
  37. package/dist/esm/address_province_bounty.d.ts +56 -0
  38. package/dist/esm/address_province_bounty.js +5560 -0
  39. package/dist/esm/country.d.ts +6 -0
  40. package/dist/esm/country.js +22 -0
  41. package/dist/esm/data.d.ts +3 -0
  42. package/dist/esm/data.js +2 -0
  43. package/dist/esm/directions.d.ts +5 -0
  44. package/dist/esm/directions.js +10 -0
  45. package/dist/esm/encoding.d.ts +3 -0
  46. package/dist/esm/encoding.js +17 -0
  47. package/dist/esm/grid.d.ts +23 -0
  48. package/dist/esm/grid.js +148 -0
  49. package/dist/esm/index.d.ts +27 -0
  50. package/dist/esm/index.js +16 -0
  51. package/dist/esm/internal.d.ts +3 -0
  52. package/dist/esm/internal.js +15 -0
  53. package/dist/esm/legacy-address.d.ts +3 -0
  54. package/dist/esm/legacy-address.js +41 -0
  55. package/dist/esm/package.json +1 -0
  56. package/dist/esm/protocol.d.ts +22 -0
  57. package/dist/esm/protocol.js +22 -0
  58. package/dist/esm/types.d.ts +114 -0
  59. package/dist/esm/types.js +1 -0
  60. package/package.json +67 -4
@@ -0,0 +1,6 @@
1
+ import type { Country, CountryOption, ProvinceOption } from "./types.js";
2
+ export declare function getCountry(countryCode: string): Country;
3
+ /** Options for a country selector. These describe the frozen snapshot. */
4
+ export declare function getCountries(): CountryOption[];
5
+ /** Country-wide grids return an empty list; unknown country codes throw. */
6
+ export declare function getProvinces(countryCode: string): ProvinceOption[];
@@ -0,0 +1,22 @@
1
+ import { countries } from "./address_bounty.js";
2
+ import { normalizeCode } from "./internal.js";
3
+ export function getCountry(countryCode) {
4
+ const code = normalizeCode(countryCode, "countryCode");
5
+ if (!Object.prototype.hasOwnProperty.call(countries, code)) {
6
+ throw new RangeError(`Country not configured: ${code}`);
7
+ }
8
+ return countries[code];
9
+ }
10
+ /** Options for a country selector. These describe the frozen snapshot. */
11
+ export function getCountries() {
12
+ return Object.entries(countries)
13
+ .map(([code, entry]) => ({ code, name: entry.name, requiresProvince: entry.provinces !== undefined }))
14
+ .sort((left, right) => left.name.localeCompare(right.name, "en") || left.code.localeCompare(right.code, "en"));
15
+ }
16
+ /** Country-wide grids return an empty list; unknown country codes throw. */
17
+ export function getProvinces(countryCode) {
18
+ const country = getCountry(countryCode);
19
+ return Object.entries(country.provinces ?? {})
20
+ .map(([code, entry]) => ({ code, name: entry.name }))
21
+ .sort((left, right) => left.name.localeCompare(right.name, "en") || left.code.localeCompare(right.code, "en"));
22
+ }
@@ -0,0 +1,3 @@
1
+ export { countries, additionalSourceAreas } from "./address_bounty.js";
2
+ export { provinceCountries, provinceGrids, provinceCounts, provinceDataNotes, DZ, CD, SD, LY, TD, NE, AO, ML, ZA, ET, MR, RU, CA, US, CN, BR, AU, IN, AR, KZ, SA, ID, } from "./address_province_bounty.js";
3
+ export type { Coordinates, Corners, Country, ProvinceCountry, Province } from "./types.js";
@@ -0,0 +1,2 @@
1
+ export { countries, additionalSourceAreas } from "./address_bounty.js";
2
+ export { provinceCountries, provinceGrids, provinceCounts, provinceDataNotes, DZ, CD, SD, LY, TD, NE, AO, ML, ZA, ET, MR, RU, CA, US, CN, BR, AU, IN, AR, KZ, SA, ID, } from "./address_province_bounty.js";
@@ -0,0 +1,5 @@
1
+ import type { Coordinates } from "./types.js";
2
+ /** Open directions to the saved GPS point, independently of display labels or protocol version. */
3
+ export declare function getDirectionsUrl(savedAddress: Readonly<{
4
+ coordinates: Coordinates;
5
+ }>): string;
@@ -0,0 +1,10 @@
1
+ import { validateCoordinates } from "./grid.js";
2
+ /** Open directions to the saved GPS point, independently of display labels or protocol version. */
3
+ export function getDirectionsUrl(savedAddress) {
4
+ if (!savedAddress || typeof savedAddress !== "object" || !savedAddress.coordinates || typeof savedAddress.coordinates !== "object") {
5
+ throw new TypeError("A saved address with coordinates is required.");
6
+ }
7
+ validateCoordinates(savedAddress.coordinates);
8
+ const { latitude, longitude } = savedAddress.coordinates;
9
+ return `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(`${latitude},${longitude}`)}`;
10
+ }
@@ -0,0 +1,3 @@
1
+ import type { AddressEncoding } from "./types.js";
2
+ /** Encode unrounded projected offsets; directions are chosen before flooring. */
3
+ export declare function encodeOffsets(northingMeters: number, eastingMeters: number): AddressEncoding;
@@ -0,0 +1,17 @@
1
+ import { ZAP_PROTOCOL } from "./protocol.js";
2
+ /** Encode unrounded projected offsets; directions are chosen before flooring. */
3
+ export function encodeOffsets(northingMeters, eastingMeters) {
4
+ const northSouthWholeMeters = Math.floor(Math.abs(northingMeters));
5
+ const eastWestWholeMeters = Math.floor(Math.abs(eastingMeters));
6
+ const northSouthRemainder = northSouthWholeMeters % ZAP_PROTOCOL.blockSizeMeters;
7
+ const eastWestRemainder = eastWestWholeMeters % ZAP_PROTOCOL.blockSizeMeters;
8
+ return {
9
+ northSouthBlock: Math.floor(northSouthWholeMeters / ZAP_PROTOCOL.blockSizeMeters),
10
+ eastWestBlock: Math.floor(eastWestWholeMeters / ZAP_PROTOCOL.blockSizeMeters),
11
+ northSouthDirection: northingMeters < 0 ? "S" : "N",
12
+ eastWestDirection: eastingMeters < 0 ? "W" : "E",
13
+ northSouthRemainder,
14
+ eastWestRemainder,
15
+ coordinateSuffix: `#${String(northSouthRemainder).padStart(2, "0")}-${String(eastWestRemainder).padStart(2, "0")}`,
16
+ };
17
+ }
@@ -0,0 +1,23 @@
1
+ import type { GridInput, Coordinates } from "./types.js";
2
+ export declare function validateCoordinates(point: Coordinates): void;
3
+ /** Shared immutable grid selection and original spherical AEQD projection. */
4
+ export declare function projectAddress(input: GridInput): {
5
+ gridName: string;
6
+ countryVersion: string;
7
+ gridVersion: string;
8
+ coordinates: Readonly<{
9
+ latitude: number;
10
+ longitude: number;
11
+ }>;
12
+ center: Readonly<{
13
+ latitude: number;
14
+ longitude: number;
15
+ }>;
16
+ widthMeters: number;
17
+ heightMeters: number;
18
+ northingMeters: number;
19
+ eastingMeters: number;
20
+ identityComponents: (string | number)[];
21
+ provinceCode?: string;
22
+ countryCode: string;
23
+ };
@@ -0,0 +1,148 @@
1
+ import { getCountry } from "./country.js";
2
+ import { normalizeCode } from "./internal.js";
3
+ import { ZAP_PROTOCOL } from "./protocol.js";
4
+ // Versioned grid bounds come from the frozen Natural Earth snapshot.
5
+ const EARTH_RADIUS_METERS = ZAP_PROTOCOL.earthRadiusMeters;
6
+ function radians(degrees) {
7
+ return (degrees * Math.PI) / 180;
8
+ }
9
+ function wrap360(degrees) {
10
+ return ((degrees % 360) + 360) % 360;
11
+ }
12
+ export function validateCoordinates(point) {
13
+ if (!Number.isFinite(point.latitude) ||
14
+ !Number.isFinite(point.longitude) ||
15
+ Math.abs(point.latitude) > 90 ||
16
+ Math.abs(point.longitude) > 180) {
17
+ throw new RangeError("Latitude must be between -90 and 90, and longitude between -180 and 180.");
18
+ }
19
+ }
20
+ function getBounds(corners) {
21
+ corners.forEach(validateCoordinates);
22
+ const [topLeft, topRight, bottomRight, bottomLeft] = corners;
23
+ const north = topLeft.latitude;
24
+ const south = bottomLeft.latitude;
25
+ const west = topLeft.longitude;
26
+ const east = topRight.longitude;
27
+ if (north <= south ||
28
+ topRight.latitude !== north ||
29
+ bottomRight.latitude !== south ||
30
+ bottomRight.longitude !== east ||
31
+ bottomLeft.longitude !== west) {
32
+ throw new Error("Corners must form a latitude/longitude-aligned rectangle.");
33
+ }
34
+ // Supports rectangles that cross the international date line.
35
+ const longitudeSpan = east - west === 360 ? 360 : wrap360(east - west);
36
+ if (longitudeSpan === 0 || (longitudeSpan >= 180 && longitudeSpan !== 360)) {
37
+ throw new Error("Rectangle longitude span must be below 180 degrees or cover all 360 degrees.");
38
+ }
39
+ return {
40
+ north,
41
+ south,
42
+ west,
43
+ longitudeSpan,
44
+ };
45
+ }
46
+ /** Shared immutable grid selection and original spherical AEQD projection. */
47
+ export function projectAddress(input) {
48
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
49
+ throw new TypeError("Address input must be an object.");
50
+ }
51
+ const { countryCode, provinceCode, latitude, longitude, } = input;
52
+ const code = normalizeCode(countryCode, "countryCode");
53
+ const selectedProvinceCode = provinceCode === undefined || provinceCode === ""
54
+ ? undefined
55
+ : normalizeCode(provinceCode, "provinceCode");
56
+ const coordinates = {
57
+ latitude,
58
+ longitude,
59
+ };
60
+ validateCoordinates(coordinates);
61
+ const country = getCountry(code);
62
+ let corners;
63
+ let gridName;
64
+ let gridVersion;
65
+ // Use the user-selected province. No automatic province matching.
66
+ if (country.provinces !== undefined) {
67
+ if (!selectedProvinceCode) {
68
+ throw new Error("Please select your province or state.");
69
+ }
70
+ if (!Object.prototype.hasOwnProperty.call(country.provinces, selectedProvinceCode)) {
71
+ throw new Error(`Province not configured: ${code} ${selectedProvinceCode}`);
72
+ }
73
+ const province = country.provinces[selectedProvinceCode];
74
+ corners = province.corners;
75
+ gridName = province.name;
76
+ gridVersion = province.version;
77
+ }
78
+ else {
79
+ if (selectedProvinceCode) {
80
+ throw new Error(`${code} uses a country-wide grid. Omit provinceCode.`);
81
+ }
82
+ corners = country.corners;
83
+ gridName = country.name;
84
+ gridVersion = country.version;
85
+ }
86
+ const prefix = selectedProvinceCode
87
+ ? `${code} ${selectedProvinceCode}`
88
+ : code;
89
+ const bounds = getBounds(corners);
90
+ // Check only the selected grid's rectangle.
91
+ // Overlap with other rectangles is allowed.
92
+ // This does not verify administrative membership.
93
+ const longitudeFromWest = wrap360(longitude - bounds.west);
94
+ if (latitude < bounds.south ||
95
+ latitude > bounds.north ||
96
+ longitudeFromWest > bounds.longitudeSpan) {
97
+ throw new RangeError(`Coordinates are outside the configured ${prefix} rectangle.`);
98
+ }
99
+ // Calculate the rectangle's center.
100
+ const center = {
101
+ latitude: (bounds.north + bounds.south) / 2,
102
+ longitude: wrap360(bounds.west + bounds.longitudeSpan / 2 + 180) - 180,
103
+ };
104
+ // Approximate rectangle dimensions on a spherical Earth.
105
+ // Width follows the middle latitude; height follows a meridian.
106
+ const widthMeters = EARTH_RADIUS_METERS *
107
+ radians(bounds.longitudeSpan) *
108
+ Math.cos(radians(center.latitude));
109
+ const heightMeters = EARTH_RADIUS_METERS * radians(bounds.north - bounds.south);
110
+ // Spherical azimuthal equidistant projection.
111
+ const lat = radians(latitude);
112
+ const originLat = radians(center.latitude);
113
+ const deltaLon = radians(longitude - center.longitude);
114
+ const eastComponent = Math.cos(lat) * Math.sin(deltaLon);
115
+ const northComponent = Math.cos(originLat) * Math.sin(lat) -
116
+ Math.sin(originLat) * Math.cos(lat) * Math.cos(deltaLon);
117
+ const cosAngle = Math.sin(originLat) * Math.sin(lat) +
118
+ Math.cos(originLat) * Math.cos(lat) * Math.cos(deltaLon);
119
+ const sinAngle = Math.hypot(eastComponent, northComponent);
120
+ const angle = Math.atan2(sinAngle, cosAngle);
121
+ if (Math.PI - angle < 1e-10) {
122
+ throw new RangeError("Location cannot be projected from this grid center.");
123
+ }
124
+ const scale = sinAngle < 1e-15 ? 1 : angle / sinAngle;
125
+ return {
126
+ countryCode: code,
127
+ ...(selectedProvinceCode ? { provinceCode: selectedProvinceCode } : {}),
128
+ gridName,
129
+ countryVersion: country.version,
130
+ gridVersion,
131
+ coordinates,
132
+ center,
133
+ widthMeters: Math.round(widthMeters),
134
+ heightMeters: Math.round(heightMeters),
135
+ // Only normalize exact zero. In particular, retain sub-meter negative signs.
136
+ northingMeters: EARTH_RADIUS_METERS * scale * northComponent || 0,
137
+ eastingMeters: EARTH_RADIUS_METERS * scale * eastComponent || 0,
138
+ identityComponents: [
139
+ prefix,
140
+ country.version,
141
+ gridVersion,
142
+ ZAP_PROTOCOL.projection,
143
+ EARTH_RADIUS_METERS,
144
+ center.latitude,
145
+ center.longitude,
146
+ ],
147
+ };
148
+ }
@@ -0,0 +1,27 @@
1
+ import { generateAddress } from "./address.js";
2
+ import { generateLegacyAddress } from "./legacy-address.js";
3
+ import { getDirectionsUrl } from "./directions.js";
4
+ import { getCountries, getCountry, getProvinces } from "./country.js";
5
+ import { ZAP_PROTOCOL, LEGACY_ZAP_PROTOCOL } from "./protocol.js";
6
+ export { generateAddress, generateLegacyAddress, getDirectionsUrl, getCountries, getCountry, getProvinces, ZAP_PROTOCOL, LEGACY_ZAP_PROTOCOL, };
7
+ export type { AddressInput, AddressResult, AddressEncoding, LegacyAddressInput, LegacyAddressResult, Coordinates, Corners, GridInput, Country, CountryOption, Province, ProvinceCountry, ProvinceOption, } from "./types.js";
8
+ /** Named namespace for applications that refer to the protocol as `zap`. */
9
+ export declare const zap: Readonly<{
10
+ protocol: Readonly<{
11
+ readonly id: "ZAP";
12
+ readonly addressSuffix: "ZAP";
13
+ readonly name: "Zukall Addressing Protocol";
14
+ readonly version: "2";
15
+ readonly projection: "AEQD-SPHERE-v1";
16
+ readonly earthRadiusMeters: 6371008.8;
17
+ readonly blockSizeMeters: 100;
18
+ readonly detailSizeMeters: 1;
19
+ readonly encoding: "blocks-100m-detail-1m-floor-absolute-dashed-suffix-2x2-v1";
20
+ }>;
21
+ generateAddress: typeof generateAddress;
22
+ generateLegacyAddress: typeof generateLegacyAddress;
23
+ getDirectionsUrl: typeof getDirectionsUrl;
24
+ getCountries: typeof getCountries;
25
+ getCountry: typeof getCountry;
26
+ getProvinces: typeof getProvinces;
27
+ }>;
@@ -0,0 +1,16 @@
1
+ import { generateAddress } from "./address.js";
2
+ import { generateLegacyAddress } from "./legacy-address.js";
3
+ import { getDirectionsUrl } from "./directions.js";
4
+ import { getCountries, getCountry, getProvinces } from "./country.js";
5
+ import { ZAP_PROTOCOL, LEGACY_ZAP_PROTOCOL } from "./protocol.js";
6
+ export { generateAddress, generateLegacyAddress, getDirectionsUrl, getCountries, getCountry, getProvinces, ZAP_PROTOCOL, LEGACY_ZAP_PROTOCOL, };
7
+ /** Named namespace for applications that refer to the protocol as `zap`. */
8
+ export const zap = Object.freeze({
9
+ protocol: ZAP_PROTOCOL,
10
+ generateAddress,
11
+ generateLegacyAddress,
12
+ getDirectionsUrl,
13
+ getCountries,
14
+ getCountry,
15
+ getProvinces,
16
+ });
@@ -0,0 +1,3 @@
1
+ /** Freeze bundled snapshots so their version cannot outlive changed grid bounds. */
2
+ export declare function deepFreeze<T>(value: T): T;
3
+ export declare function normalizeCode(value: unknown, field: string): string;
@@ -0,0 +1,15 @@
1
+ /** Freeze bundled snapshots so their version cannot outlive changed grid bounds. */
2
+ export function deepFreeze(value) {
3
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
4
+ for (const child of Object.values(value))
5
+ deepFreeze(child);
6
+ Object.freeze(value);
7
+ }
8
+ return value;
9
+ }
10
+ export function normalizeCode(value, field) {
11
+ if (typeof value !== "string" || !value.trim()) {
12
+ throw new TypeError(`${field} must be a nonempty string.`);
13
+ }
14
+ return value.trim().toUpperCase();
15
+ }
@@ -0,0 +1,3 @@
1
+ import type { LegacyAddressInput, LegacyAddressResult } from "./types.js";
2
+ /** Explicit version-1 reproduction. Never use this to reinterpret a version-2 record. */
3
+ export declare function generateLegacyAddress(input: LegacyAddressInput): LegacyAddressResult;
@@ -0,0 +1,41 @@
1
+ import { projectAddress } from "./grid.js";
2
+ import { LEGACY_ZAP_PROTOCOL } from "./protocol.js";
3
+ /** Explicit version-1 reproduction. Never use this to reinterpret a version-2 record. */
4
+ export function generateLegacyAddress(input) {
5
+ const grid = projectAddress(input);
6
+ const gridSizeMeters = input.gridSizeMeters === undefined
7
+ ? LEGACY_ZAP_PROTOCOL.defaultGridSizeMeters : input.gridSizeMeters;
8
+ if (!Number.isSafeInteger(gridSizeMeters) || gridSizeMeters < 1 || gridSizeMeters > 1000) {
9
+ throw new RangeError("Grid size must be an integer between 1 and 1000 meters.");
10
+ }
11
+ const snap = (meters) => Math.round(meters / gridSizeMeters) * gridSizeMeters || 0;
12
+ const northingMeters = snap(grid.northingMeters);
13
+ const eastingMeters = snap(grid.eastingMeters);
14
+ return {
15
+ protocol: LEGACY_ZAP_PROTOCOL.id,
16
+ protocolVersion: LEGACY_ZAP_PROTOCOL.version,
17
+ address: [
18
+ `${Math.abs(northingMeters)} ${northingMeters < 0 ? "S" : "N"} ${Math.abs(eastingMeters)} ${eastingMeters < 0 ? "W" : "E"}`,
19
+ ...(grid.provinceCode ? [grid.provinceCode] : []),
20
+ grid.countryCode,
21
+ LEGACY_ZAP_PROTOCOL.addressSuffix,
22
+ ].join(", "),
23
+ countryCode: grid.countryCode,
24
+ ...(grid.provinceCode ? { provinceCode: grid.provinceCode } : {}),
25
+ gridName: grid.gridName,
26
+ coordinates: grid.coordinates,
27
+ center: grid.center,
28
+ widthMeters: grid.widthMeters,
29
+ heightMeters: grid.heightMeters,
30
+ northingMeters,
31
+ eastingMeters,
32
+ gridSizeMeters,
33
+ gridId: [
34
+ LEGACY_ZAP_PROTOCOL.id,
35
+ LEGACY_ZAP_PROTOCOL.version,
36
+ ...grid.identityComponents,
37
+ gridSizeMeters,
38
+ "nearest-positive-tie",
39
+ ].join(":"),
40
+ };
41
+ }
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,22 @@
1
+ /** ZAP means Zukall Addressing Protocol. Protocol and npm package versions are separate. */
2
+ export declare const ZAP_PROTOCOL: Readonly<{
3
+ readonly id: "ZAP";
4
+ readonly addressSuffix: "ZAP";
5
+ readonly name: "Zukall Addressing Protocol";
6
+ readonly version: "2";
7
+ readonly projection: "AEQD-SPHERE-v1";
8
+ readonly earthRadiusMeters: 6371008.8;
9
+ readonly blockSizeMeters: 100;
10
+ readonly detailSizeMeters: 1;
11
+ readonly encoding: "blocks-100m-detail-1m-floor-absolute-dashed-suffix-2x2-v1";
12
+ }>;
13
+ /** Frozen version-1 rules, for explicitly reproducing previously issued addresses. */
14
+ export declare const LEGACY_ZAP_PROTOCOL: Readonly<{
15
+ readonly id: "ZAP";
16
+ readonly addressSuffix: "ZAP";
17
+ readonly name: "Zukall Address Protocol";
18
+ readonly version: "1";
19
+ readonly projection: "AEQD-SPHERE-v1";
20
+ readonly earthRadiusMeters: 6371008.8;
21
+ readonly defaultGridSizeMeters: 5;
22
+ }>;
@@ -0,0 +1,22 @@
1
+ /** ZAP means Zukall Addressing Protocol. Protocol and npm package versions are separate. */
2
+ export const ZAP_PROTOCOL = Object.freeze({
3
+ id: "ZAP",
4
+ addressSuffix: "ZAP",
5
+ name: "Zukall Addressing Protocol",
6
+ version: "2",
7
+ projection: "AEQD-SPHERE-v1",
8
+ earthRadiusMeters: 6_371_008.8,
9
+ blockSizeMeters: 100,
10
+ detailSizeMeters: 1,
11
+ encoding: "blocks-100m-detail-1m-floor-absolute-dashed-suffix-2x2-v1",
12
+ });
13
+ /** Frozen version-1 rules, for explicitly reproducing previously issued addresses. */
14
+ export const LEGACY_ZAP_PROTOCOL = Object.freeze({
15
+ id: "ZAP",
16
+ addressSuffix: "ZAP",
17
+ name: "Zukall Address Protocol",
18
+ version: "1",
19
+ projection: "AEQD-SPHERE-v1",
20
+ earthRadiusMeters: 6_371_008.8,
21
+ defaultGridSizeMeters: 5,
22
+ });
@@ -0,0 +1,114 @@
1
+ /** WGS84 latitude and longitude in decimal degrees. */
2
+ export type Coordinates = Readonly<{
3
+ latitude: number;
4
+ longitude: number;
5
+ }>;
6
+ export type Corners = readonly [
7
+ topLeft: Coordinates,
8
+ topRight: Coordinates,
9
+ bottomRight: Coordinates,
10
+ bottomLeft: Coordinates
11
+ ];
12
+ export type Province = Readonly<{
13
+ name: string;
14
+ version: string;
15
+ corners: Corners;
16
+ }>;
17
+ /** Original source snapshot, with the singular `province` field. */
18
+ export type ProvinceCountry = Readonly<{
19
+ name: string;
20
+ version: string;
21
+ province: Readonly<Record<string, Province>>;
22
+ }>;
23
+ /** Configuration used by the generator, with the plural `provinces` field. */
24
+ export type Country = Readonly<{
25
+ name: string;
26
+ version: string;
27
+ }> & ({
28
+ readonly corners: Corners;
29
+ readonly provinces?: never;
30
+ } | {
31
+ readonly corners?: never;
32
+ readonly provinces: Readonly<Record<string, Province>>;
33
+ });
34
+ export type GridInput = {
35
+ countryCode: string;
36
+ /** Required for countries that use province grids; omitted for other countries. */
37
+ provinceCode?: string;
38
+ latitude: number;
39
+ longitude: number;
40
+ };
41
+ export type AddressInput = GridInput & {
42
+ /** Optional display label; never changes the grid origin. */
43
+ city?: string;
44
+ /** Separate display metadata; omitted from the coordinate address text. */
45
+ street?: string;
46
+ /** Removed in protocol 2. Use generateLegacyAddress for protocol-1 records. */
47
+ gridSizeMeters?: never;
48
+ };
49
+ export type LegacyAddressInput = GridInput & {
50
+ /** Whole meters, from 1 through 1000. Defaults to 5. */
51
+ gridSizeMeters?: number;
52
+ };
53
+ export type AddressEncoding = Readonly<{
54
+ northSouthBlock: number;
55
+ eastWestBlock: number;
56
+ northSouthDirection: "N" | "S";
57
+ eastWestDirection: "E" | "W";
58
+ /** Whole-meter magnitude within the block, from 0 through 99. */
59
+ northSouthRemainder: number;
60
+ eastWestRemainder: number;
61
+ /** Two digits per remainder, north/south first, e.g. #03-06. */
62
+ coordinateSuffix: string;
63
+ }>;
64
+ export type AddressResult = Readonly<{
65
+ protocol: "ZAP";
66
+ protocolVersion: "2";
67
+ address: string;
68
+ countryCode: string;
69
+ provinceCode?: string;
70
+ city?: string;
71
+ street?: string;
72
+ countryVersion: string;
73
+ gridVersion: string;
74
+ gridName: string;
75
+ coordinates: Coordinates;
76
+ center: Coordinates;
77
+ widthMeters: number;
78
+ heightMeters: number;
79
+ /** Unrounded signed projected offsets from the original GPS coordinates. */
80
+ northingMeters: number;
81
+ eastingMeters: number;
82
+ blockSizeMeters: 100;
83
+ detailSizeMeters: 1;
84
+ encoding: "blocks-100m-detail-1m-floor-absolute-dashed-suffix-2x2-v1";
85
+ /** Persist with the address to identify the snapshot, projection, and encoding. */
86
+ gridId: string;
87
+ }> & AddressEncoding;
88
+ /** Original protocol-1 result; its offsets remain snapped to gridSizeMeters. */
89
+ export type LegacyAddressResult = Readonly<{
90
+ protocol: "ZAP";
91
+ protocolVersion: "1";
92
+ address: string;
93
+ countryCode: string;
94
+ provinceCode?: string;
95
+ gridName: string;
96
+ coordinates: Coordinates;
97
+ center: Coordinates;
98
+ widthMeters: number;
99
+ heightMeters: number;
100
+ northingMeters: number;
101
+ eastingMeters: number;
102
+ gridSizeMeters: number;
103
+ /** Persist with the address to identify the snapshot and projection rules. */
104
+ gridId: string;
105
+ }>;
106
+ export type CountryOption = Readonly<{
107
+ code: string;
108
+ name: string;
109
+ requiresProvince: boolean;
110
+ }>;
111
+ export type ProvinceOption = Readonly<{
112
+ code: string;
113
+ name: string;
114
+ }>;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "@zukall/zap",
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": "ZAP (Zukall Addressing Protocol): deterministic 100-meter grid blocks with whole-meter detail from GPS coordinates.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/cjs/index.js",
8
+ "module": "./dist/esm/index.js",
9
+ "types": "./dist/esm/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/esm/index.d.ts",
14
+ "default": "./dist/esm/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/cjs/index.d.ts",
18
+ "default": "./dist/cjs/index.js"
19
+ }
20
+ },
21
+ "./data": {
22
+ "import": {
23
+ "types": "./dist/esm/data.d.ts",
24
+ "default": "./dist/esm/data.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/cjs/data.d.ts",
28
+ "default": "./dist/cjs/data.js"
29
+ }
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "PROTOCOL.md",
37
+ "PROTOCOL-V1.md",
38
+ "LICENSE",
39
+ "THIRD-PARTY-NOTICES.md"
40
+ ],
41
+ "sideEffects": false,
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "keywords": [
46
+ "zap",
47
+ "zukall",
48
+ "zukall-addressing-protocol",
49
+ "address",
50
+ "geolocation",
51
+ "coordinates",
52
+ "grid",
53
+ "typescript"
54
+ ],
55
+ "scripts": {
56
+ "build": "node scripts/build.mjs",
57
+ "typecheck": "tsc --noEmit -p tsconfig.json",
58
+ "test": "npm run build && node --test",
59
+ "check": "npm run typecheck && npm test",
60
+ "prepack": "npm run check",
61
+ "pack:check": "node scripts/verify-package.mjs"
62
+ },
63
+ "devDependencies": {
64
+ "typescript": "5.9.3"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public"
68
+ }
69
+ }