@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 +1 -1
- package/README.md +243 -14
- package/asset/zukall-haiti-country-grid.webp +0 -0
- package/dist/cjs/distance.d.ts +8 -0
- package/dist/cjs/distance.js +61 -0
- package/dist/cjs/index.d.ts +11 -2
- package/dist/cjs/index.js +9 -1
- package/dist/cjs/locations.d.ts +11 -0
- package/dist/cjs/locations.js +67 -0
- package/dist/cjs/route.d.ts +10 -0
- package/dist/cjs/route.js +289 -0
- package/dist/cjs/types.d.ts +70 -0
- package/dist/esm/distance.d.ts +8 -0
- package/dist/esm/distance.js +55 -0
- package/dist/esm/index.d.ts +11 -2
- package/dist/esm/index.js +6 -1
- package/dist/esm/locations.d.ts +11 -0
- package/dist/esm/locations.js +61 -0
- package/dist/esm/route.d.ts +10 -0
- package/dist/esm/route.js +285 -0
- package/dist/esm/types.d.ts +70 -0
- package/package.json +2 -2
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { getDistanceMethod, haversineMeters, requireFiniteDistance } from "./distance.js";
|
|
2
|
+
import { getLocationCoordinates, getProjectedPoint, resolveLocationPoints, validateLocationOptions } from "./locations.js";
|
|
3
|
+
/** Work limits are separate from address protocol/configuration versions. */
|
|
4
|
+
export const ROUTE_OPTIMIZATION = Object.freeze({
|
|
5
|
+
exactMaxStops: 12, maxStops: 1000, twoOptMaxPasses: 100,
|
|
6
|
+
});
|
|
7
|
+
const at = (matrix, from, to) => matrix[from][to];
|
|
8
|
+
function validateEndpoint(index, count, name) {
|
|
9
|
+
if (index !== undefined && (!Number.isSafeInteger(index) || index < 0 || index >= count)) {
|
|
10
|
+
throw new RangeError(`${name} must be an original input index within points.`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function validateRouteOptions(points, options) {
|
|
14
|
+
validateLocationOptions(options);
|
|
15
|
+
if (!Array.isArray(points))
|
|
16
|
+
throw new TypeError("points must be an array of locations.");
|
|
17
|
+
if (points.length > ROUTE_OPTIMIZATION.maxStops) {
|
|
18
|
+
throw new RangeError(`At most ${ROUTE_OPTIMIZATION.maxStops} stops are supported per itinerary.`);
|
|
19
|
+
}
|
|
20
|
+
for (let index = 0; index < points.length; index++) {
|
|
21
|
+
if (!(index in points))
|
|
22
|
+
throw new TypeError("points must not contain empty slots.");
|
|
23
|
+
}
|
|
24
|
+
validateEndpoint(options.startIndex, points.length, "startIndex");
|
|
25
|
+
validateEndpoint(options.endIndex, points.length, "endIndex");
|
|
26
|
+
if (options.returnToStart !== undefined && typeof options.returnToStart !== "boolean") {
|
|
27
|
+
throw new TypeError("returnToStart must be a boolean.");
|
|
28
|
+
}
|
|
29
|
+
if (options.returnToStart && options.endIndex !== undefined) {
|
|
30
|
+
throw new RangeError("endIndex cannot be combined with returnToStart.");
|
|
31
|
+
}
|
|
32
|
+
if (points.length > 1 && options.endIndex === (options.startIndex ?? 0)) {
|
|
33
|
+
throw new RangeError("An open itinerary must have distinct startIndex/endIndex; use returnToStart for a round trip.");
|
|
34
|
+
}
|
|
35
|
+
getDistanceMethod(options.method);
|
|
36
|
+
if (options.distanceMatrix !== undefined && options.method !== undefined) {
|
|
37
|
+
throw new TypeError("distanceMatrix cannot be combined with method.");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function validateMatrix(matrix, count) {
|
|
41
|
+
if (!Array.isArray(matrix) || matrix.length !== count) {
|
|
42
|
+
throw new RangeError("distanceMatrix must have one row and column per original input stop.");
|
|
43
|
+
}
|
|
44
|
+
// Reserve headroom so summing every leg is finite, including a closing leg.
|
|
45
|
+
const maximum = Number.MAX_VALUE / (count + 1);
|
|
46
|
+
return Array.from({ length: count }, (_, rowIndex) => {
|
|
47
|
+
const row = matrix[rowIndex];
|
|
48
|
+
if (!Array.isArray(row) || row.length !== count) {
|
|
49
|
+
throw new RangeError("distanceMatrix must have one row and column per original input stop.");
|
|
50
|
+
}
|
|
51
|
+
return Array.from({ length: count }, (_, columnIndex) => {
|
|
52
|
+
const value = row[columnIndex];
|
|
53
|
+
if (!Number.isFinite(value) || value < 0 || value > maximum || (rowIndex === columnIndex && value !== 0)) {
|
|
54
|
+
throw new RangeError("distanceMatrix entries must be finite nonnegative meters with a zero diagonal and finite route totals.");
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function buildMatrix(points, options) {
|
|
61
|
+
const coordinates = points.map(getLocationCoordinates);
|
|
62
|
+
if (options.distanceMatrix !== undefined) {
|
|
63
|
+
return { matrix: validateMatrix(options.distanceMatrix, points.length), method: "distance-matrix" };
|
|
64
|
+
}
|
|
65
|
+
const method = getDistanceMethod(options.method);
|
|
66
|
+
const projected = method === "projected" ? points.map(getProjectedPoint) : undefined;
|
|
67
|
+
const gridId = projected?.[0]?.gridId;
|
|
68
|
+
if (projected?.some(point => point.gridId !== gridId)) {
|
|
69
|
+
throw new RangeError("Projected distance requires exactly the same gridId for every stop.");
|
|
70
|
+
}
|
|
71
|
+
const matrix = Array.from({ length: points.length }, () => Array(points.length).fill(0));
|
|
72
|
+
for (let left = 0; left < points.length; left++) {
|
|
73
|
+
for (let right = left + 1; right < points.length; right++) {
|
|
74
|
+
const meters = requireFiniteDistance(projected ? Math.hypot(projected[right].northingMeters - projected[left].northingMeters, projected[right].eastingMeters - projected[left].eastingMeters) : haversineMeters(coordinates[left], coordinates[right]));
|
|
75
|
+
if (meters > Number.MAX_VALUE / (points.length + 1))
|
|
76
|
+
throw new RangeError("Route distances must have finite totals.");
|
|
77
|
+
matrix[left][right] = meters;
|
|
78
|
+
matrix[right][left] = meters;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { matrix, method, ...(gridId !== undefined ? { gridId } : {}) };
|
|
82
|
+
}
|
|
83
|
+
function routeCost(order, matrix, closed) {
|
|
84
|
+
let total = 0;
|
|
85
|
+
for (let index = 1; index < order.length; index++)
|
|
86
|
+
total += at(matrix, order[index - 1], order[index]);
|
|
87
|
+
if (closed && order.length > 1)
|
|
88
|
+
total += at(matrix, order[order.length - 1], order[0]);
|
|
89
|
+
return requireFiniteDistance(total);
|
|
90
|
+
}
|
|
91
|
+
function feasibleOrder(count, start, end) {
|
|
92
|
+
if (count === 0)
|
|
93
|
+
return [];
|
|
94
|
+
const order = [start];
|
|
95
|
+
for (let index = 0; index < count; index++)
|
|
96
|
+
if (index !== start && index !== end)
|
|
97
|
+
order.push(index);
|
|
98
|
+
if (end !== undefined && end !== start)
|
|
99
|
+
order.push(end);
|
|
100
|
+
return order;
|
|
101
|
+
}
|
|
102
|
+
/** Held–Karp subset dynamic programming, including directed matrices and endpoint constraints. */
|
|
103
|
+
function exactOrder(matrix, start, end, closed) {
|
|
104
|
+
const count = matrix.length;
|
|
105
|
+
const free = Array.from({ length: count }, (_, index) => index).filter(index => index !== start && index !== end);
|
|
106
|
+
const size = free.length;
|
|
107
|
+
if (size === 0)
|
|
108
|
+
return feasibleOrder(count, start, end);
|
|
109
|
+
const states = 1 << size;
|
|
110
|
+
const costs = new Float64Array(states * size).fill(Infinity);
|
|
111
|
+
const keys = new Float64Array(states * size).fill(Infinity);
|
|
112
|
+
const parents = new Int16Array(states * size).fill(-1);
|
|
113
|
+
// Up to 12 base-13 digits fit exactly in a JS integer; keys settle equal-cost ties by input index.
|
|
114
|
+
const base = count + 1;
|
|
115
|
+
for (let last = 0; last < size; last++) {
|
|
116
|
+
const state = (1 << last) * size + last;
|
|
117
|
+
costs[state] = at(matrix, start, free[last]);
|
|
118
|
+
keys[state] = (start + 1) * base + free[last] + 1;
|
|
119
|
+
}
|
|
120
|
+
for (let mask = 1; mask < states; mask++) {
|
|
121
|
+
for (let last = 0; last < size; last++) {
|
|
122
|
+
if (!(mask & (1 << last)))
|
|
123
|
+
continue;
|
|
124
|
+
const state = mask * size + last;
|
|
125
|
+
if (!Number.isFinite(costs[state]))
|
|
126
|
+
continue;
|
|
127
|
+
for (let next = 0; next < size; next++) {
|
|
128
|
+
if (mask & (1 << next))
|
|
129
|
+
continue;
|
|
130
|
+
const nextState = (mask | (1 << next)) * size + next;
|
|
131
|
+
const candidate = costs[state] + at(matrix, free[last], free[next]);
|
|
132
|
+
const key = keys[state] * base + free[next] + 1;
|
|
133
|
+
if (candidate < costs[nextState] || (candidate === costs[nextState] && key < keys[nextState])) {
|
|
134
|
+
costs[nextState] = candidate;
|
|
135
|
+
keys[nextState] = key;
|
|
136
|
+
parents[nextState] = last;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const full = states - 1;
|
|
142
|
+
let bestCost = Infinity;
|
|
143
|
+
let bestKey = Infinity;
|
|
144
|
+
let last = -1;
|
|
145
|
+
for (let candidate = 0; candidate < size; candidate++) {
|
|
146
|
+
const state = full * size + candidate;
|
|
147
|
+
const destination = end ?? (closed ? start : undefined);
|
|
148
|
+
const cost = costs[state] + (destination === undefined ? 0 : at(matrix, free[candidate], destination));
|
|
149
|
+
if (cost < bestCost || (cost === bestCost && keys[state] < bestKey)) {
|
|
150
|
+
bestCost = cost;
|
|
151
|
+
bestKey = keys[state];
|
|
152
|
+
last = candidate;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const reversed = [];
|
|
156
|
+
let mask = full;
|
|
157
|
+
while (last >= 0) {
|
|
158
|
+
reversed.push(free[last]);
|
|
159
|
+
const previous = parents[mask * size + last];
|
|
160
|
+
mask ^= 1 << last;
|
|
161
|
+
last = previous;
|
|
162
|
+
}
|
|
163
|
+
return [start, ...reversed.reverse(), ...(end === undefined ? [] : [end])];
|
|
164
|
+
}
|
|
165
|
+
function nearestNeighbor(matrix, start, end) {
|
|
166
|
+
const remaining = new Set(Array.from({ length: matrix.length }, (_, index) => index).filter(index => index !== start && index !== end));
|
|
167
|
+
const order = [start];
|
|
168
|
+
while (remaining.size) {
|
|
169
|
+
const current = order[order.length - 1];
|
|
170
|
+
let next = -1;
|
|
171
|
+
let best = Infinity;
|
|
172
|
+
for (const candidate of remaining) {
|
|
173
|
+
const cost = at(matrix, current, candidate);
|
|
174
|
+
if (cost < best || (cost === best && candidate < next)) {
|
|
175
|
+
best = cost;
|
|
176
|
+
next = candidate;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
order.push(next);
|
|
180
|
+
remaining.delete(next);
|
|
181
|
+
}
|
|
182
|
+
if (end !== undefined)
|
|
183
|
+
order.push(end);
|
|
184
|
+
return order;
|
|
185
|
+
}
|
|
186
|
+
/** Fixed-start 2-opt; directed reversal costs are included, not just the two boundary edges. */
|
|
187
|
+
function twoOpt(seed, matrix, closed, fixedEnd) {
|
|
188
|
+
let order = [...seed];
|
|
189
|
+
let total = routeCost(order, matrix, closed);
|
|
190
|
+
for (let pass = 0; pass < ROUTE_OPTIMIZATION.twoOptMaxPasses; pass++) {
|
|
191
|
+
const reverseCosts = Array(order.length).fill(0);
|
|
192
|
+
for (let index = 0; index < order.length - 1; index++) {
|
|
193
|
+
reverseCosts[index + 1] = reverseCosts[index] + at(matrix, order[index + 1], order[index]) - at(matrix, order[index], order[index + 1]);
|
|
194
|
+
}
|
|
195
|
+
let improved = false;
|
|
196
|
+
const maximumEnd = order.length - (fixedEnd ? 2 : 1);
|
|
197
|
+
for (let first = 1; first < maximumEnd && !improved; first++) {
|
|
198
|
+
for (let last = first + 1; last <= maximumEnd; last++) {
|
|
199
|
+
const before = order[first - 1];
|
|
200
|
+
const head = order[first];
|
|
201
|
+
const tail = order[last];
|
|
202
|
+
const after = order[last + 1] ?? (closed ? order[0] : undefined);
|
|
203
|
+
const delta = at(matrix, before, tail) - at(matrix, before, head)
|
|
204
|
+
+ (after === undefined ? 0 : at(matrix, head, after) - at(matrix, tail, after))
|
|
205
|
+
+ reverseCosts[last] - reverseCosts[first];
|
|
206
|
+
if (delta >= 0)
|
|
207
|
+
continue;
|
|
208
|
+
const candidate = [...order.slice(0, first), ...order.slice(first, last + 1).reverse(), ...order.slice(last + 1)];
|
|
209
|
+
const candidateCost = routeCost(candidate, matrix, closed);
|
|
210
|
+
if (candidateCost < total) {
|
|
211
|
+
order = candidate;
|
|
212
|
+
total = candidateCost;
|
|
213
|
+
improved = true;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!improved)
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
return order;
|
|
222
|
+
}
|
|
223
|
+
function lexicographicallyBefore(left, right) {
|
|
224
|
+
for (let index = 0; index < left.length; index++) {
|
|
225
|
+
if (left[index] !== right[index])
|
|
226
|
+
return left[index] < right[index];
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
function optimizeResolved(inputs, points, options) {
|
|
231
|
+
const { matrix, method, gridId } = buildMatrix(points, options);
|
|
232
|
+
const start = options.startIndex ?? 0;
|
|
233
|
+
const end = options.endIndex;
|
|
234
|
+
const closed = options.returnToStart ?? false;
|
|
235
|
+
const baseline = feasibleOrder(points.length, start, end);
|
|
236
|
+
const originalTotalMeters = routeCost(baseline, matrix, closed);
|
|
237
|
+
const exact = points.length <= ROUTE_OPTIMIZATION.exactMaxStops;
|
|
238
|
+
let order = baseline;
|
|
239
|
+
if (points.length > 1) {
|
|
240
|
+
if (exact)
|
|
241
|
+
order = exactOrder(matrix, start, end, closed);
|
|
242
|
+
else {
|
|
243
|
+
// Optimizing both seeds and retaining the baseline guarantees no regression against supplied order.
|
|
244
|
+
let bestCost = originalTotalMeters;
|
|
245
|
+
for (const seed of [baseline, nearestNeighbor(matrix, start, end)]) {
|
|
246
|
+
const candidate = twoOpt(seed, matrix, closed, end !== undefined);
|
|
247
|
+
const cost = routeCost(candidate, matrix, closed);
|
|
248
|
+
if (cost < bestCost || (cost === bestCost && lexicographicallyBefore(candidate, order))) {
|
|
249
|
+
order = candidate;
|
|
250
|
+
bestCost = cost;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const legs = [];
|
|
256
|
+
const addLeg = (fromIndex, toIndex, isReturn) => {
|
|
257
|
+
const meters = at(matrix, fromIndex, toIndex);
|
|
258
|
+
legs.push({ fromIndex, toIndex, meters, kilometers: meters / 1000, method, isReturn });
|
|
259
|
+
};
|
|
260
|
+
for (let index = 1; index < order.length; index++)
|
|
261
|
+
addLeg(order[index - 1], order[index], false);
|
|
262
|
+
if (closed && order.length > 1)
|
|
263
|
+
addLeg(order[order.length - 1], order[0], true);
|
|
264
|
+
const totalMeters = routeCost(order, matrix, closed);
|
|
265
|
+
return {
|
|
266
|
+
orderedStops: order.map(index => inputs[index]), originalIndices: order,
|
|
267
|
+
resolvedPoints: order.map(index => points[index]), legs, totalMeters, totalKilometers: totalMeters / 1000,
|
|
268
|
+
method, ...(gridId !== undefined ? { gridId } : {}), optimization: exact ? "exact" : "approximate",
|
|
269
|
+
algorithm: points.length < 2 ? "trivial" : exact ? "held-karp" : "nearest-neighbor-2-opt",
|
|
270
|
+
exactCutoff: ROUTE_OPTIMIZATION.exactMaxStops, returnToStart: closed, originalTotalMeters,
|
|
271
|
+
description: method === "distance-matrix" ? "optimized distance-matrix itinerary" : "optimized straight-line itinerary",
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** Reorder stops without mutating them or changing any issued address/grid identity. */
|
|
275
|
+
export function optimizeRoute(points, options = {}) {
|
|
276
|
+
validateRouteOptions(points, options);
|
|
277
|
+
// Snapshot array order before asynchronous lookups; retain each original stop reference.
|
|
278
|
+
const inputs = [...points];
|
|
279
|
+
const settings = { ...options };
|
|
280
|
+
if (settings.distanceMatrix !== undefined)
|
|
281
|
+
settings.distanceMatrix = validateMatrix(settings.distanceMatrix, inputs.length);
|
|
282
|
+
const resolved = resolveLocationPoints(inputs, settings.resolveAddress);
|
|
283
|
+
return resolved instanceof Promise
|
|
284
|
+
? resolved.then(locations => optimizeResolved(inputs, locations, settings)) : optimizeResolved(inputs, resolved, settings);
|
|
285
|
+
}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -112,3 +112,73 @@ export type ProvinceOption = Readonly<{
|
|
|
112
112
|
code: string;
|
|
113
113
|
name: string;
|
|
114
114
|
}>;
|
|
115
|
+
/** A saved location, including either address protocol's complete result. */
|
|
116
|
+
export type SavedLocationPoint = Readonly<{
|
|
117
|
+
coordinates: Coordinates;
|
|
118
|
+
}>;
|
|
119
|
+
export type LocationPoint = Coordinates | SavedLocationPoint;
|
|
120
|
+
export type LocationInput = LocationPoint | string;
|
|
121
|
+
/** Resolve an exact address string to its saved original GPS record. */
|
|
122
|
+
export type AddressResolver = (address: string, originalIndex: number) => SavedLocationPoint | PromiseLike<SavedLocationPoint>;
|
|
123
|
+
export type DistanceMethod = "haversine" | "projected";
|
|
124
|
+
export type RouteDistanceMethod = DistanceMethod | "distance-matrix";
|
|
125
|
+
export type DistanceOptions = {
|
|
126
|
+
/** Defaults to Haversine from the original GPS point. */
|
|
127
|
+
method?: DistanceMethod;
|
|
128
|
+
resolveAddress?: never;
|
|
129
|
+
};
|
|
130
|
+
/** Supplying a resolver makes the operation asynchronous, even for a cache lookup. */
|
|
131
|
+
export type ResolvedDistanceOptions = Omit<DistanceOptions, "resolveAddress"> & {
|
|
132
|
+
resolveAddress: AddressResolver;
|
|
133
|
+
};
|
|
134
|
+
export type DistanceResult = Readonly<{
|
|
135
|
+
meters: number;
|
|
136
|
+
kilometers: number;
|
|
137
|
+
}> & ({
|
|
138
|
+
readonly method: "haversine";
|
|
139
|
+
} | {
|
|
140
|
+
readonly method: "projected";
|
|
141
|
+
readonly gridId: string;
|
|
142
|
+
});
|
|
143
|
+
/** Meter costs indexed by original input index; directed/asymmetric matrices are supported. */
|
|
144
|
+
export type DistanceMatrix = readonly (readonly number[])[];
|
|
145
|
+
export type RouteOptions = DistanceOptions & {
|
|
146
|
+
/** Original input index of the fixed start; defaults to 0 for a nonempty list. */
|
|
147
|
+
startIndex?: number;
|
|
148
|
+
/** Original input index of the fixed end; incompatible with returnToStart. */
|
|
149
|
+
endIndex?: number;
|
|
150
|
+
returnToStart?: boolean;
|
|
151
|
+
/** Optional caller-supplied costs in meters; cannot be combined with method. */
|
|
152
|
+
distanceMatrix?: DistanceMatrix;
|
|
153
|
+
};
|
|
154
|
+
export type ResolvedRouteOptions = Omit<RouteOptions, "resolveAddress"> & {
|
|
155
|
+
resolveAddress: AddressResolver;
|
|
156
|
+
};
|
|
157
|
+
export type RouteLeg = Readonly<{
|
|
158
|
+
fromIndex: number;
|
|
159
|
+
toIndex: number;
|
|
160
|
+
meters: number;
|
|
161
|
+
kilometers: number;
|
|
162
|
+
method: RouteDistanceMethod;
|
|
163
|
+
/** The closing leg; the start is not duplicated in orderedStops. */
|
|
164
|
+
isReturn: boolean;
|
|
165
|
+
}>;
|
|
166
|
+
export type RouteResult<T extends LocationInput = LocationInput> = Readonly<{
|
|
167
|
+
/** Original inputs, retaining object identity and including coincident stops separately. */
|
|
168
|
+
orderedStops: readonly T[];
|
|
169
|
+
originalIndices: readonly number[];
|
|
170
|
+
/** Resolved GPS points/records in the same order as orderedStops. */
|
|
171
|
+
resolvedPoints: readonly LocationPoint[];
|
|
172
|
+
legs: readonly RouteLeg[];
|
|
173
|
+
totalMeters: number;
|
|
174
|
+
totalKilometers: number;
|
|
175
|
+
method: RouteDistanceMethod;
|
|
176
|
+
gridId?: string;
|
|
177
|
+
optimization: "exact" | "approximate";
|
|
178
|
+
algorithm: "trivial" | "held-karp" | "nearest-neighbor-2-opt";
|
|
179
|
+
exactCutoff: 12;
|
|
180
|
+
returnToStart: boolean;
|
|
181
|
+
/** Cost of supplied order with the requested start moved first and end moved last. */
|
|
182
|
+
originalTotalMeters: number;
|
|
183
|
+
description: "optimized straight-line itinerary" | "optimized distance-matrix itinerary";
|
|
184
|
+
}>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zukall/zap",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "ZAP (Zukall Addressing Protocol):
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "ZAP (Zukall Addressing Protocol): GPS addresses, geographic distance, and optimized straight-line itineraries.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/cjs/index.js",
|