@zukall/zap 0.2.2 → 0.3.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/PROTOCOL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # ZAP — Zukall Addressing Protocol, version 2
2
2
 
3
3
  Protocol 2 uses **100-meter grid blocks** and **1-meter detail**, computed from
4
- unrounded projected GPS coordinates. The npm package version (`0.2.2`) and
4
+ unrounded projected GPS coordinates. The npm package version (`0.3.0`) and
5
5
  protocol version (`2`) are separate. Previously issued protocol-1 addresses retain
6
6
  their original text and identity; see [PROTOCOL-V1.md](PROTOCOL-V1.md).
7
7
 
package/README.md CHANGED
@@ -1,19 +1,51 @@
1
1
  # ZAP — Zukall Addressing Protocol
2
2
 
3
3
  **`@zukall/zap`** generates addresses directly from latitude and longitude using
4
- fixed country/province grids. Protocol **2** encodes **100-meter blocks** and
5
- separate **whole-meter remainders**. It needs no Google Places API, geocoding
6
- service, network request, or runtime dependency. Generation works offline.
4
+ fixed country/province grids.
5
+
6
+ **Give real places a recognizable address, without waiting for street names or
7
+ house numbers.**
8
+
9
+ Imagine building a courier application that serves Uganda, Haiti, Florida in
10
+ the United States, and Germany. Your couriers and remote dispatchers need to
11
+ recognize addresses quickly, even when those places use different languages,
12
+ street names, and numbering systems.
13
+
14
+ With ZAP, they learn one address format and use it across the configured
15
+ countries and states. A home, business, rural entrance, or pickup point can have
16
+ an address based on its GPS location. A dispatcher can read the same address
17
+ structure in each market. Your application can show a courier where a
18
+ destination lies relative to their current location in the same grid, then open
19
+ map directions using the destination’s saved GPS point.
20
+
21
+ Once you have the GPS coordinates, you can generate the address offline,
22
+ without a geocoding service, API key, or per-address fee. ZAP works alongside
23
+ existing street addresses and also gives you a location address where street
24
+ names or house numbers are missing.
7
25
 
8
26
  ## See how ZAP works: Haiti’s grid
9
27
 
10
- These illustrations show locations in the same fixed Haiti grid. Each address
11
- is derived independently from its coordinates. The road can run north–south,
12
- east–west, diagonally, or curve; the grid origin and axes stay fixed.
28
+ ZAP starts with a configured rectangle around a country. Where a country uses
29
+ province or state grids, it uses the selected province’s rectangle instead—for
30
+ example, Florida’s grid within the United States.
31
+
32
+ Each rectangle has a fixed center and two axes: north/south and east/west.
33
+ Think of laying a grid of **100 × 100-meter square blocks** across the area.
34
+ The main address numbers count complete blocks from the center, and the
35
+ direction letters tell you which side of the center the location is on.
36
+
37
+ A suffix such as **`#23-43`** adds the position within that block: 23 whole meters
38
+ along the address’s north/south direction and 43 along its east/west direction.
39
+ That gives a home, entrance, or pickup point a location code with whole-meter
40
+ detail. Keep apartment and unit numbers alongside the code as separate details.
41
+
42
+ The Haiti illustrations below show the same fixed grid in use. Each address
43
+ comes from its own GPS point. Roads can run straight, diagonally, or curve;
44
+ the grid’s center and axes stay fixed.
13
45
 
14
46
  ### Whole-country grid and fixed center
15
47
 
16
- ![Haiti’s entire configured ZAP grid, its fixed center in the Gulf of Gonâve, and a GPS-to-address example](https://cdn.jsdelivr.net/npm/@zukall/zap@0.2.2/asset/zukall-haiti-country-grid.webp)
48
+ ![Haiti’s entire configured ZAP grid, its fixed center in the Gulf of Gonâve, and a GPS-to-address example](https://cdn.jsdelivr.net/npm/@zukall/zap@0.2.3/asset/zukall-haiti-country-grid.webp)
17
49
 
18
50
  The Haiti grid has one fixed center: **19.0578675° N, 73.0641385° W**.
19
51
  It is the center of the configured rectangle, in the Gulf of Gonâve, rather than
@@ -44,7 +76,7 @@ The examples and captions below use the current dashed format.
44
76
 
45
77
  ### North–south: easting stays fixed
46
78
 
47
- ![Haiti ZAP grid with five north–south locations, constant easting, and 100-meter projected intervals](https://cdn.jsdelivr.net/npm/@zukall/zap@0.2.2/asset/zukall-north-south-example.webp)
79
+ ![Haiti ZAP grid with five north–south locations, constant easting, and 100-meter projected intervals](https://cdn.jsdelivr.net/npm/@zukall/zap@0.2.3/asset/zukall-north-south-example.webp)
48
80
 
49
81
  ```text
50
82
  Point A: 580 S 772 E #42-68, HT ZAP
@@ -58,7 +90,7 @@ Moving north on this side of the origin decreases the south block.
58
90
 
59
91
  ### East–west: northing stays fixed
60
92
 
61
- ![Haiti ZAP grid with five east–west locations, constant south offset, and 100-meter projected intervals](https://cdn.jsdelivr.net/npm/@zukall/zap@0.2.2/asset/zukall-west-east.webp)
93
+ ![Haiti ZAP grid with five east–west locations, constant south offset, and 100-meter projected intervals](https://cdn.jsdelivr.net/npm/@zukall/zap@0.2.3/asset/zukall-west-east.webp)
62
94
 
63
95
  ```text
64
96
  Point A: 582 S 770 E #42-68, HT ZAP
@@ -72,7 +104,7 @@ Moving west on this side of the origin decreases the east block.
72
104
 
73
105
  ### Diagonal streets: both axes can change
74
106
 
75
- ![Haiti ZAP grid over a diagonal street, showing changing south and east offsets with a fixed grid](https://cdn.jsdelivr.net/npm/@zukall/zap@0.2.2/asset/zukall-diagnol-example.webp)
107
+ ![Haiti ZAP grid over a diagonal street, showing changing south and east offsets with a fixed grid](https://cdn.jsdelivr.net/npm/@zukall/zap@0.2.3/asset/zukall-diagnol-example.webp)
76
108
 
77
109
  ```text
78
110
  Point A: 580 S 770 E #43-46, HT ZAP
@@ -191,7 +223,7 @@ npm install @zukall/zap
191
223
  Before publication, install the verified local package:
192
224
 
193
225
  ```sh
194
- npm install /absolute/path/to/zukall-address-protocol/.artifacts/zukall-zap-0.2.2.tgz
226
+ npm install /absolute/path/to/zukall-address-protocol/.artifacts/zukall-zap-0.3.0.tgz
195
227
  ```
196
228
 
197
229
  Build and verify the source:
@@ -449,6 +481,196 @@ API key. You can also pass `saved.coordinates` to your own map provider. Never
449
481
  use the formatted ZAP string, grid center, city name, or coordinate suffix as the
450
482
  routing destination.
451
483
 
484
+ ## Measure distance between locations
485
+
486
+ **ZAP’s strongest advantage here is making distance understandable to people;
487
+ software can handle either easily.** Within one grid, a difference of three
488
+ blocks along an axis represents 300 projected meters. That is easier for a
489
+ person to read than comparing decimal GPS coordinates. Software can calculate
490
+ distance from either representation.
491
+
492
+ Use complete saved records whenever available. `getDistance` defaults to the
493
+ original GPS coordinates, including when the records come from different
494
+ countries, states, or protocol versions:
495
+
496
+ ```js
497
+ import { generateAddress, getDistance } from "@zukall/zap";
498
+
499
+ const depot = generateAddress({
500
+ countryCode: "UG", city: "Kampala", latitude: 0.3476, longitude: 32.5825,
501
+ });
502
+ const pickup = generateAddress({
503
+ countryCode: "UG", city: "Kampala", latitude: 0.3486, longitude: 32.5825,
504
+ });
505
+
506
+ const distance = getDistance(depot, pickup);
507
+ console.log(distance.method); // haversine
508
+ console.log(distance.meters.toFixed(1)); // 111.2
509
+ console.log(distance.kilometers.toFixed(4)); // 0.1112
510
+
511
+ // Latitude/longitude objects also work.
512
+ getDistance(depot.coordinates, pickup.coordinates);
513
+
514
+ // Optional distance in the saved grid's projected plane.
515
+ const gridDistance = getDistance(depot, pickup, { method: "projected" });
516
+ console.log(gridDistance.method); // projected
517
+ console.log(gridDistance.gridId === depot.gridId); // true
518
+ ```
519
+
520
+ Haversine measures the geographic great-circle distance on a sphere, using the
521
+ same **6,371,008.8-meter Earth radius** as the address projection. Values retain
522
+ number precision; rounding above is only for display. It does not measure road
523
+ length, altitude, or terrain.
524
+
525
+ Projected mode uses `Math.hypot()` on the stored northing/easting differences.
526
+ Both records must have **exactly the same `gridId`** and finite offsets. A
527
+ different country, province, snapshot, or encoding ID is rejected. This mode
528
+ does not regenerate addresses or reproject older records; protocol-1 offsets
529
+ can retain their original snapping. Projected-plane distances can differ from
530
+ geographic distances away from the grid origin. Neither mode parses the address
531
+ text, subtracts unit numbers, or treats the suffix as a house number.
532
+
533
+ ## Optimize randomly entered stops
534
+
535
+ `optimizeRoute` returns an **optimized straight-line itinerary**. It chooses an
536
+ order that reduces the sum of distances between consecutive stops. Your
537
+ application can use that order to plan visits, then open map directions from
538
+ each stop's saved GPS point. It does not provide driving directions or guarantee
539
+ the shortest journey along roads.
540
+
541
+ This example uses illustrative GPS points along a meridian, entered out of order:
542
+
543
+ ```js
544
+ import { generateAddress, optimizeRoute } from "@zukall/zap";
545
+
546
+ const stop = (id, latitude) => ({
547
+ id,
548
+ ...generateAddress({
549
+ countryCode: "UG", city: "Kampala", latitude, longitude: 32.5825,
550
+ }),
551
+ });
552
+ const stops = [
553
+ stop("depot", 0.3476),
554
+ stop("C", 0.3506),
555
+ stop("A", 0.3486),
556
+ stop("B", 0.3496),
557
+ ];
558
+
559
+ const itinerary = optimizeRoute(stops);
560
+ console.log(itinerary.description); // optimized straight-line itinerary
561
+ console.log(itinerary.orderedStops.map(stop => stop.id)); // ["depot", "A", "B", "C"]
562
+ console.log(itinerary.originalIndices); // [0, 2, 3, 1]
563
+ console.log(itinerary.totalMeters.toFixed(1)); // 333.6
564
+ console.log(itinerary.originalTotalMeters.toFixed(1)); // 667.2
565
+ console.log(itinerary.optimization); // exact
566
+ console.log(itinerary.algorithm); // held-karp
567
+ console.log(itinerary.method); // haversine
568
+
569
+ for (const leg of itinerary.legs) {
570
+ console.log(stops[leg.fromIndex].id, stops[leg.toIndex].id, leg.meters);
571
+ }
572
+
573
+ // Select endpoints by their ORIGINAL input indices.
574
+ const fixedEndpoints = optimizeRoute(stops, { startIndex: 2, endIndex: 1 });
575
+ // Starts at A (input 2), finishes at C (input 1), and includes every other stop.
576
+
577
+ const roundTrip = optimizeRoute(stops, { returnToStart: true });
578
+ console.log(roundTrip.legs.at(-1).isReturn); // true
579
+ // orderedStops still includes the depot only once; legs include the return to it.
580
+ ```
581
+
582
+ | Option | Behavior |
583
+ | --- | --- |
584
+ | `startIndex` | Fixed start selected from the supplied stops; defaults to input index `0`. |
585
+ | `endIndex` | Optional fixed final stop, selected by original index. |
586
+ | `returnToStart` | Adds a closing leg to the start; defaults to `false` and cannot be combined with `endIndex`. |
587
+ | `method` | `"haversine"` by default, or `"projected"` when every saved record has the identical grid ID. |
588
+ | `resolveAddress` | Caller-owned lookup for address strings; supplying it makes the call return a Promise. |
589
+ | `distanceMatrix` | Optional meter costs indexed by original stop indices; cannot be combined with `method`. |
590
+
591
+ Endpoints must be supplied stops. If a depot is outside your delivery list,
592
+ include it in `points` and select its index. An open itinerary with multiple
593
+ stops cannot have the same start and end; choose `returnToStart` for that case.
594
+
595
+ The result contains the original objects or strings in `orderedStops`, their
596
+ `originalIndices`, `resolvedPoints` in the same order, and `legs` with
597
+ `fromIndex`, `toIndex`, meters, kilometers, method, and `isReturn`. Totals are
598
+ available as `totalMeters` and `totalKilometers`. Inputs and saved addresses are
599
+ not modified. Duplicate coordinates and duplicate references remain separate
600
+ stops and retain their own indices.
601
+
602
+ ### Exact and approximate optimization
603
+
604
+ - **Up to 12 stops:** Held–Karp subset dynamic programming finds an exact minimum
605
+ for the selected distance costs and endpoint constraints.
606
+ - **13–1000 stops:** Deterministic nearest neighbor followed by 2-opt returns an
607
+ approximate order. Both the nearest-neighbor order and the original feasible
608
+ order are improved, with at most 100 2-opt passes per seed. Fixed endpoints
609
+ remain fixed. Results are never longer than the original feasible order.
610
+
611
+ `optimization` reports `"exact"` or `"approximate"`; `algorithm` names the
612
+ algorithm, and `exactCutoff` is `12`. Equal-cost choices use original input
613
+ indices deterministically. `ROUTE_OPTIMIZATION` exports the frozen work limits.
614
+ The original feasible order moves the selected start first and end last, keeping
615
+ the other stops in their entered order; its cost is `originalTotalMeters`.
616
+
617
+ Empty lists and single stops return an exact zero-distance itinerary with no
618
+ legs, even for a round trip. Explicit endpoint indices must still be valid.
619
+ Invalid coordinates, sparse arrays, contradictory endpoints, and lists above
620
+ 1000 stops are rejected. Optimizing many stops uses a quadratic distance matrix;
621
+ approximate results are not a guarantee of the best possible order.
622
+
623
+ ### Address strings need saved-record lookups
624
+
625
+ The package has **no reverse decoder or automatic geocoder**. A text-only ZAP
626
+ address cannot restore the original GPS point. Supply a resolver that retrieves
627
+ the corresponding saved record; it may return the record directly or use an
628
+ asynchronous database lookup:
629
+
630
+ ```js
631
+ const savedByAddress = new Map(stops.map(record => [record.address, record]));
632
+ const resolveAddress = async (address, originalIndex) => {
633
+ const record = savedByAddress.get(address);
634
+ if (!record) throw new Error(`No saved GPS record for input ${originalIndex}`);
635
+ return record;
636
+ };
637
+
638
+ const textStops = stops.map(record => record.address);
639
+ const itineraryFromText = await optimizeRoute(textStops, { resolveAddress });
640
+ const distanceFromText = await getDistance(textStops[0], textStops[1], { resolveAddress });
641
+ ```
642
+
643
+ Resolvers receive each string and its original input index (0/1 for
644
+ `getDistance`), once per string occurrence. The original strings remain in
645
+ `orderedStops`; retrieved records are in `resolvedPoints`. A resolver must return
646
+ a saved record with valid `coordinates`; missing records and lookup failures
647
+ reject the Promise. Direct GPS/record calls without a resolver stay synchronous.
648
+ Use full records and your own stop IDs when addresses are shared by different
649
+ units or records; an address string alone does not identify a unique household.
650
+
651
+ ### Supplying distance costs from another provider
652
+
653
+ The optimizer consumes a distance matrix internally. Your application can supply
654
+ one from a future road-distance provider without changing stop identities:
655
+
656
+ ```js
657
+ const matrix = [
658
+ [0, 400, 100],
659
+ [350, 0, 200],
660
+ [120, 220, 0],
661
+ ];
662
+ const suppliedCosts = optimizeRoute(stops.slice(0, 3), { distanceMatrix: matrix });
663
+ console.log(suppliedCosts.method); // distance-matrix
664
+ console.log(suppliedCosts.description); // optimized distance-matrix itinerary
665
+ ```
666
+
667
+ Rows/columns follow **original input order**, entries are nonnegative finite
668
+ **meters**, and the diagonal must be zero. Matrices may be asymmetric; both
669
+ algorithms account for the full cost of reversing directed legs. Values must
670
+ leave room for finite route totals. The package does not fetch provider data.
671
+ Results are labeled separately when caller-supplied costs are used. The function
672
+ still returns a stop order, not driving instructions or a vehicle scheduling plan.
673
+
452
674
  ## Upgrade from protocol 1 without changing issued addresses
453
675
 
454
676
  Package `0.2.0` introduces protocol 2. `generateAddress` no longer accepts
@@ -544,6 +766,9 @@ See [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) and [PROTOCOL.md](PROTOCOL.
544
766
  | `generateAddress(input)` | Generate a protocol-2 address with fixed 100-meter blocks and 1-meter detail. |
545
767
  | `generateLegacyAddress(input)` | Explicitly reproduce the original protocol-1 address and grid ID. |
546
768
  | `getDirectionsUrl(savedRecord)` | Build directions using original saved GPS coordinates, for either version. |
769
+ | `getDistance(pointA, pointB, options?)` | Haversine meters/kilometers from GPS, or projected distance within one exact grid ID. |
770
+ | `optimizeRoute(points, options?)` | Exact or approximate stop order, original indices, legs and totals for an optimized straight-line itinerary. |
771
+ | `ROUTE_OPTIMIZATION` | Inspect the 12-stop exact cutoff, 1000-stop limit and 100-pass heuristic work bound. |
547
772
  | `getCountries()` / `getCountry(code)` | Select or inspect immutable country configurations. |
548
773
  | `getProvinces(code)` | List configured provinces; country-wide grids return `[]`. |
549
774
  | `ZAP_PROTOCOL` / `LEGACY_ZAP_PROTOCOL` | Read current or original protocol constants. |
@@ -562,7 +787,11 @@ Tests cover the requested formats, all directions, zero and sub-meter negatives,
562
787
  99/100-meter boundaries, two-digit suffixes, country/province selection, unchanged
563
788
  grid identity components, date-line rectangles, diagonal sequences, 100-meter
564
789
  axis movements, old-address compatibility, saved GPS directions, and all centers
565
- and corners of 747 configured grids. Package verification checks ESM/CommonJS
790
+ and corners of 747 configured grids. Distance/itinerary tests check known
791
+ distances, mixed grids, endpoints, return legs, duplicate identities, explicit
792
+ resolvers, and exact solutions against an independent exhaustive search.
793
+ Heuristics are checked against the original feasible order, including directed
794
+ matrices. Package verification checks ESM/CommonJS
566
795
  runtime exports and compiles both TypeScript consumer formats, including the
567
796
  removed-option error.
568
797
 
@@ -571,11 +800,11 @@ removed-option error.
571
800
  ```sh
572
801
  npm login
573
802
  npm run pack:check
574
- npm publish ./.artifacts/zukall-zap-0.2.2.tgz --access public
803
+ npm publish ./.artifacts/zukall-zap-0.3.0.tgz --access public
575
804
  ```
576
805
 
577
806
  An npm account with permission to publish `@zukall/zap` is required. Building and
578
- packing do not publish the package. Package version is `0.2.2`; current protocol
807
+ packing do not publish the package. Package version is `0.3.0`; current protocol
579
808
  version is `2`, while preserved legacy results remain version `1`.
580
809
 
581
810
  ## License
@@ -0,0 +1,8 @@
1
+ import type { Coordinates, DistanceMethod, DistanceOptions, DistanceResult, LocationInput, LocationPoint, ResolvedDistanceOptions } from "./types.js";
2
+ export declare function getDistanceMethod(method: DistanceMethod | undefined): DistanceMethod;
3
+ /** Geographic great-circle surface distance, using the existing spherical Earth radius. */
4
+ export declare function haversineMeters(pointA: Coordinates, pointB: Coordinates): number;
5
+ export declare function requireFiniteDistance(meters: number): number;
6
+ export declare function getDistance(pointA: LocationPoint, pointB: LocationPoint, options?: DistanceOptions): DistanceResult;
7
+ export declare function getDistance(pointA: LocationInput, pointB: LocationInput, options: ResolvedDistanceOptions): Promise<DistanceResult>;
8
+ export declare function getDistance(pointA: LocationPoint, pointB: LocationPoint, options: DistanceOptions | ResolvedDistanceOptions): DistanceResult | Promise<DistanceResult>;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDistanceMethod = getDistanceMethod;
4
+ exports.haversineMeters = haversineMeters;
5
+ exports.requireFiniteDistance = requireFiniteDistance;
6
+ exports.getDistance = getDistance;
7
+ const protocol_js_1 = require("./protocol.js");
8
+ const locations_js_1 = require("./locations.js");
9
+ function getDistanceMethod(method) {
10
+ if (method !== undefined && method !== "haversine" && method !== "projected") {
11
+ throw new TypeError("method must be haversine or projected.");
12
+ }
13
+ return method ?? "haversine";
14
+ }
15
+ /** Geographic great-circle surface distance, using the existing spherical Earth radius. */
16
+ function haversineMeters(pointA, pointB) {
17
+ const radians = (degrees) => degrees * Math.PI / 180;
18
+ const latitudeA = radians(pointA.latitude);
19
+ const latitudeB = radians(pointB.latitude);
20
+ let longitudeDifference = pointB.longitude - pointA.longitude;
21
+ if (longitudeDifference > 180)
22
+ longitudeDifference -= 360;
23
+ if (longitudeDifference < -180)
24
+ longitudeDifference += 360;
25
+ const cosineA = Math.abs(pointA.latitude) === 90 ? 0 : Math.cos(latitudeA);
26
+ const cosineB = Math.abs(pointB.latitude) === 90 ? 0 : Math.cos(latitudeB);
27
+ const haversine = Math.sin(radians(pointB.latitude - pointA.latitude) / 2) ** 2
28
+ + cosineA * cosineB * Math.sin(radians(longitudeDifference) / 2) ** 2;
29
+ const clamped = Math.max(0, Math.min(1, haversine));
30
+ return 2 * protocol_js_1.ZAP_PROTOCOL.earthRadiusMeters * Math.atan2(Math.sqrt(clamped), Math.sqrt(1 - clamped));
31
+ }
32
+ function requireFiniteDistance(meters) {
33
+ if (!Number.isFinite(meters) || meters < 0)
34
+ throw new RangeError("Distance must be finite and nonnegative.");
35
+ return meters === 0 ? 0 : meters;
36
+ }
37
+ function calculateDistance(points, method) {
38
+ const pointA = points[0];
39
+ const pointB = points[1];
40
+ const coordinatesA = (0, locations_js_1.getLocationCoordinates)(pointA);
41
+ const coordinatesB = (0, locations_js_1.getLocationCoordinates)(pointB);
42
+ if (method === "projected") {
43
+ const gridA = (0, locations_js_1.getProjectedPoint)(pointA);
44
+ const gridB = (0, locations_js_1.getProjectedPoint)(pointB);
45
+ if (gridA.gridId !== gridB.gridId) {
46
+ throw new RangeError("Projected distance requires exactly the same gridId.");
47
+ }
48
+ const meters = requireFiniteDistance(Math.hypot(gridB.northingMeters - gridA.northingMeters, gridB.eastingMeters - gridA.eastingMeters));
49
+ return { method, meters, kilometers: meters / 1000, gridId: gridA.gridId };
50
+ }
51
+ const meters = requireFiniteDistance(haversineMeters(coordinatesA, coordinatesB));
52
+ return { method, meters, kilometers: meters / 1000 };
53
+ }
54
+ /** GPS by default; supplying a resolver returns a Promise and never silently geocodes text. */
55
+ function getDistance(pointA, pointB, options = {}) {
56
+ (0, locations_js_1.validateLocationOptions)(options);
57
+ const method = getDistanceMethod(options.method);
58
+ const resolved = (0, locations_js_1.resolveLocationPoints)([pointA, pointB], options.resolveAddress);
59
+ return resolved instanceof Promise
60
+ ? resolved.then(points => calculateDistance(points, method)) : calculateDistance(resolved, method);
61
+ }
@@ -1,10 +1,12 @@
1
1
  import { generateAddress } from "./address.js";
2
2
  import { generateLegacyAddress } from "./legacy-address.js";
3
3
  import { getDirectionsUrl } from "./directions.js";
4
+ import { getDistance } from "./distance.js";
5
+ import { optimizeRoute, ROUTE_OPTIMIZATION } from "./route.js";
4
6
  import { getCountries, getCountry, getProvinces } from "./country.js";
5
7
  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
+ export { generateAddress, generateLegacyAddress, getDirectionsUrl, getDistance, optimizeRoute, ROUTE_OPTIMIZATION, getCountries, getCountry, getProvinces, ZAP_PROTOCOL, LEGACY_ZAP_PROTOCOL, };
9
+ export type { AddressInput, AddressResult, AddressEncoding, LegacyAddressInput, LegacyAddressResult, Coordinates, Corners, GridInput, Country, CountryOption, Province, ProvinceCountry, ProvinceOption, SavedLocationPoint, LocationPoint, LocationInput, AddressResolver, DistanceMethod, RouteDistanceMethod, DistanceOptions, ResolvedDistanceOptions, DistanceResult, DistanceMatrix, RouteOptions, ResolvedRouteOptions, RouteLeg, RouteResult, } from "./types.js";
8
10
  /** Named namespace for applications that refer to the protocol as `zap`. */
9
11
  export declare const zap: Readonly<{
10
12
  protocol: Readonly<{
@@ -21,6 +23,13 @@ export declare const zap: Readonly<{
21
23
  generateAddress: typeof generateAddress;
22
24
  generateLegacyAddress: typeof generateLegacyAddress;
23
25
  getDirectionsUrl: typeof getDirectionsUrl;
26
+ getDistance: typeof getDistance;
27
+ optimizeRoute: typeof optimizeRoute;
28
+ routeOptimization: Readonly<{
29
+ readonly exactMaxStops: 12;
30
+ readonly maxStops: 1000;
31
+ readonly twoOptMaxPasses: 100;
32
+ }>;
24
33
  getCountries: typeof getCountries;
25
34
  getCountry: typeof getCountry;
26
35
  getProvinces: typeof getProvinces;
package/dist/cjs/index.js CHANGED
@@ -1,12 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.zap = exports.LEGACY_ZAP_PROTOCOL = exports.ZAP_PROTOCOL = exports.getProvinces = exports.getCountry = exports.getCountries = exports.getDirectionsUrl = exports.generateLegacyAddress = exports.generateAddress = void 0;
3
+ exports.zap = exports.LEGACY_ZAP_PROTOCOL = exports.ZAP_PROTOCOL = exports.getProvinces = exports.getCountry = exports.getCountries = exports.ROUTE_OPTIMIZATION = exports.optimizeRoute = exports.getDistance = exports.getDirectionsUrl = exports.generateLegacyAddress = exports.generateAddress = void 0;
4
4
  const address_js_1 = require("./address.js");
5
5
  Object.defineProperty(exports, "generateAddress", { enumerable: true, get: function () { return address_js_1.generateAddress; } });
6
6
  const legacy_address_js_1 = require("./legacy-address.js");
7
7
  Object.defineProperty(exports, "generateLegacyAddress", { enumerable: true, get: function () { return legacy_address_js_1.generateLegacyAddress; } });
8
8
  const directions_js_1 = require("./directions.js");
9
9
  Object.defineProperty(exports, "getDirectionsUrl", { enumerable: true, get: function () { return directions_js_1.getDirectionsUrl; } });
10
+ const distance_js_1 = require("./distance.js");
11
+ Object.defineProperty(exports, "getDistance", { enumerable: true, get: function () { return distance_js_1.getDistance; } });
12
+ const route_js_1 = require("./route.js");
13
+ Object.defineProperty(exports, "optimizeRoute", { enumerable: true, get: function () { return route_js_1.optimizeRoute; } });
14
+ Object.defineProperty(exports, "ROUTE_OPTIMIZATION", { enumerable: true, get: function () { return route_js_1.ROUTE_OPTIMIZATION; } });
10
15
  const country_js_1 = require("./country.js");
11
16
  Object.defineProperty(exports, "getCountries", { enumerable: true, get: function () { return country_js_1.getCountries; } });
12
17
  Object.defineProperty(exports, "getCountry", { enumerable: true, get: function () { return country_js_1.getCountry; } });
@@ -20,6 +25,9 @@ exports.zap = Object.freeze({
20
25
  generateAddress: address_js_1.generateAddress,
21
26
  generateLegacyAddress: legacy_address_js_1.generateLegacyAddress,
22
27
  getDirectionsUrl: directions_js_1.getDirectionsUrl,
28
+ getDistance: distance_js_1.getDistance,
29
+ optimizeRoute: route_js_1.optimizeRoute,
30
+ routeOptimization: route_js_1.ROUTE_OPTIMIZATION,
23
31
  getCountries: country_js_1.getCountries,
24
32
  getCountry: country_js_1.getCountry,
25
33
  getProvinces: country_js_1.getProvinces,
@@ -0,0 +1,11 @@
1
+ import type { AddressResolver, Coordinates, LocationInput, LocationPoint, SavedLocationPoint } from "./types.js";
2
+ export declare function validateLocationOptions(options: unknown): void;
3
+ /** Read original GPS only; never parse text, grid blocks, unit numbers, or suffixes. */
4
+ export declare function getLocationCoordinates(point: LocationPoint): Coordinates;
5
+ export declare function resolveLocationPoints(points: readonly LocationInput[], resolver: AddressResolver | undefined): LocationPoint[] | Promise<LocationPoint[]>;
6
+ export type ProjectedPoint = SavedLocationPoint & Readonly<{
7
+ gridId: string;
8
+ northingMeters: number;
9
+ eastingMeters: number;
10
+ }>;
11
+ export declare function getProjectedPoint(point: LocationPoint): ProjectedPoint;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateLocationOptions = validateLocationOptions;
4
+ exports.getLocationCoordinates = getLocationCoordinates;
5
+ exports.resolveLocationPoints = resolveLocationPoints;
6
+ exports.getProjectedPoint = getProjectedPoint;
7
+ const grid_js_1 = require("./grid.js");
8
+ function validateLocationOptions(options) {
9
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
10
+ throw new TypeError("Options must be an object.");
11
+ }
12
+ if ("resolveAddress" in options && options.resolveAddress !== undefined && typeof options.resolveAddress !== "function") {
13
+ throw new TypeError("resolveAddress must be a function.");
14
+ }
15
+ }
16
+ /** Read original GPS only; never parse text, grid blocks, unit numbers, or suffixes. */
17
+ function getLocationCoordinates(point) {
18
+ if (!point || typeof point !== "object" || Array.isArray(point)) {
19
+ throw new TypeError("A latitude/longitude object or a saved record with coordinates is required.");
20
+ }
21
+ const coordinates = "coordinates" in point ? point.coordinates : point;
22
+ if (!coordinates || typeof coordinates !== "object" || Array.isArray(coordinates)) {
23
+ throw new TypeError("A saved record must contain its original GPS coordinates.");
24
+ }
25
+ (0, grid_js_1.validateCoordinates)(coordinates);
26
+ return { latitude: coordinates.latitude, longitude: coordinates.longitude };
27
+ }
28
+ function validateResolvedRecord(record) {
29
+ if (!record || typeof record !== "object" || Array.isArray(record) || !("coordinates" in record)) {
30
+ throw new TypeError("resolveAddress must return a saved record with original GPS coordinates.");
31
+ }
32
+ getLocationCoordinates(record);
33
+ return record;
34
+ }
35
+ function resolveLocationPoints(points, resolver) {
36
+ const checkString = (point) => {
37
+ if (!point.trim())
38
+ throw new TypeError("An address string must be nonempty.");
39
+ };
40
+ if (resolver !== undefined) {
41
+ // Promise.all installs rejection handlers for every asynchronous lookup.
42
+ return Promise.all(points.map(async (point, originalIndex) => {
43
+ if (typeof point === "string") {
44
+ checkString(point);
45
+ return validateResolvedRecord(await resolver(point, originalIndex));
46
+ }
47
+ getLocationCoordinates(point);
48
+ return point;
49
+ }));
50
+ }
51
+ return points.map(point => {
52
+ if (typeof point === "string") {
53
+ checkString(point);
54
+ throw new TypeError("Address strings require resolveAddress; ZAP has no reverse decoder or automatic geocoder.");
55
+ }
56
+ getLocationCoordinates(point);
57
+ return point;
58
+ });
59
+ }
60
+ function getProjectedPoint(point) {
61
+ if (!("coordinates" in point) || !("gridId" in point) || typeof point.gridId !== "string" || !point.gridId.trim()
62
+ || !("northingMeters" in point) || !Number.isFinite(point.northingMeters)
63
+ || !("eastingMeters" in point) || !Number.isFinite(point.eastingMeters)) {
64
+ throw new TypeError("Projected distance requires saved records with a gridId and finite northingMeters/eastingMeters.");
65
+ }
66
+ return point;
67
+ }
@@ -0,0 +1,10 @@
1
+ import type { LocationInput, LocationPoint, ResolvedRouteOptions, RouteOptions, RouteResult } from "./types.js";
2
+ /** Work limits are separate from address protocol/configuration versions. */
3
+ export declare const ROUTE_OPTIMIZATION: Readonly<{
4
+ readonly exactMaxStops: 12;
5
+ readonly maxStops: 1000;
6
+ readonly twoOptMaxPasses: 100;
7
+ }>;
8
+ export declare function optimizeRoute<T extends LocationPoint>(points: readonly T[], options?: RouteOptions): RouteResult<T>;
9
+ export declare function optimizeRoute<T extends LocationInput>(points: readonly T[], options: ResolvedRouteOptions): Promise<RouteResult<T>>;
10
+ export declare function optimizeRoute<T extends LocationPoint>(points: readonly T[], options: RouteOptions | ResolvedRouteOptions): RouteResult<T> | Promise<RouteResult<T>>;