@asteby/storefront 0.3.0 → 0.4.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/README.md CHANGED
@@ -126,6 +126,57 @@ Images are `https://picsum.photos/seed/<handle>/800/800` placeholders built by
126
126
  `MOCK_IMAGE_URL`; pass `createMockStorefront({ imageUrl })` to swap them.
127
127
  `mock.reset()` restores the seed and `mock.state` exposes the live data.
128
128
 
129
+ ## Geo helpers (`@asteby/storefront/geo`)
130
+
131
+ Pure TypeScript (no network, no DOM; Node 20, edge and browsers). Behaviour is
132
+ pinned by `src/geo-vectors.json`, shared with the Go implementation.
133
+
134
+ ```ts
135
+ import {
136
+ distanceKm, rankByDistance, formatDistance, directionsUrl, parseLatLng,
137
+ openStatus, formatOpeningHours, toSchemaOpeningHours,
138
+ } from '@asteby/storefront/geo'
139
+
140
+ const origin = parseLatLng('19.4326,-99.1332') // null when malformed or (0,0)
141
+ const near = rankByDistance(shop.branches, origin!, { radiusKm: 25, limit: 3 })
142
+ // [{ item, distanceKm }] sorted by distance, ties keep input order.
143
+ // Items without a valid point (null, out of range or the "unset" (0,0)) are excluded.
144
+ formatDistance(near[0]!.distanceKm, 'es-MX') // "300 m" (50 m steps) or "4.2 km"
145
+ directionsUrl(near[0]!.item, { provider: 'google' }) // coordinates, else address text
146
+
147
+ const hours = { weekly: [{ day: 1, open: '09:00', close: '19:00' }, /* 1 = Mon ... 7 = Sun */] }
148
+ openStatus(hours, 'America/Mexico_City') // { open, closesAt, nextOpenAt } as ISO instants
149
+ formatOpeningHours(hours, 'es-MX') // "Lun–Vie 9:00–19:00 · Sáb 9:00–14:00"
150
+ toSchemaOpeningHours(hours) // ["Mo-Fr 09:00-19:00", "Sa 09:00-14:00"]
151
+ ```
152
+
153
+ `openStatus` honors intervals that cross midnight, several intervals per day,
154
+ dated `exceptions` and daylight saving time (uses `Intl.DateTimeFormat`; throws
155
+ `RangeError` for an unknown time zone). `ShopBranch` has optional
156
+ `opening_hours`, `timezone` and `opening_hours_text`; they arrive with Storefront
157
+ API `locations.v1` and are not populated yet.
158
+
159
+ ## Locations (`client.locations`, Storefront API `locations.v1`)
160
+
161
+ ```ts
162
+ const { nodes } = await sf.locations.list() // visible branches
163
+ const byCp = await sf.locations.list({ postal_code: '06000' }) // ordered by distance_km
164
+ const one = await sf.locations.get('br_centro') // null when unknown
165
+ const near = await sf.locations.nearest({ lat: 19.4326, lng: -99.1332 }) // closest first, nodes[i].distance_km
166
+ ```
167
+
168
+ `Location` is a `ShopBranch` whose structured fields are always present: `kind`,
169
+ `postal_code`, `timezone`, `opening_hours`, `services`, `contacts` and
170
+ `pickup { enabled, fee, prep_days }` (`fee: null` = free). `nearest` is
171
+ `private, no-store`: the server rounds the point to 0.01° and never stores it,
172
+ and the client neither logs nor keeps it (hooks such as `onRequest` do see the
173
+ URL, so do not log it). To rank branches you already have without sending the
174
+ point anywhere, use `rankByDistance` from `/geo` on `shop.branches`.
175
+ The mock serves the three routes with three example branches (CDMX, Monterrey,
176
+ Guadalajara) ordered by haversine; it knows the centroid of a few postal codes
177
+ (`MOCK_POSTAL_CENTROIDS`), pickup is offered only where `pickup.enabled`, and
178
+ the pickup `fee` is charged on the cart.
179
+
129
180
  ## API
130
181
 
131
182
  `createStorefrontClient(options)`:
@@ -0,0 +1,144 @@
1
+ // src/geo.ts
2
+ var RAD = Math.PI / 180;
3
+ function distanceKm(a, b) {
4
+ const dLat = (b.lat - a.lat) * RAD;
5
+ const dLng = (b.lng - a.lng) * RAD;
6
+ const h = Math.sin(dLat / 2) ** 2 + Math.cos(a.lat * RAD) * Math.cos(b.lat * RAD) * Math.sin(dLng / 2) ** 2;
7
+ return 2 * 6371.0088 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
8
+ }
9
+ function isValidPoint(p) {
10
+ const { lat, lng } = p ?? {};
11
+ return Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat) <= 90 && Math.abs(lng) <= 180 && !(lat === 0 && lng === 0);
12
+ }
13
+ function parseLatLng(s) {
14
+ const [a, b, ...rest] = (s ?? "").split(",");
15
+ const p = !rest.length && a?.trim() && b?.trim() ? { lat: Number(a), lng: Number(b) } : null;
16
+ return isValidPoint(p) ? p : null;
17
+ }
18
+ function rankByDistance(items, origin, { radiusKm, limit, getPoint = (i) => i } = {}) {
19
+ const out = [];
20
+ for (const item of items) {
21
+ const p = getPoint(item);
22
+ if (!isValidPoint(p)) continue;
23
+ const d = distanceKm(origin, p);
24
+ if (radiusKm === void 0 || d <= radiusKm) out.push({ item, distanceKm: d });
25
+ }
26
+ out.sort((x, y) => x.distanceKm - y.distanceKm);
27
+ return limit === void 0 ? out : out.slice(0, Math.max(0, limit));
28
+ }
29
+ function formatDistance(km, locale = "es-MX") {
30
+ const m = Math.round(km * 1e3 / 50) * 50;
31
+ const [value, unit, digits] = m < 1e3 ? [m, "meter", 0] : [km, "kilometer", 1];
32
+ return new Intl.NumberFormat(locale, {
33
+ style: "unit",
34
+ unit,
35
+ minimumFractionDigits: digits,
36
+ maximumFractionDigits: digits
37
+ }).format(value);
38
+ }
39
+ function directionsUrl(location, { provider = "google" } = {}) {
40
+ const ll = isValidPoint(location) ? `${location.lat},${location.lng}` : null;
41
+ const text = [location.address, location.city].filter(Boolean).join(", ");
42
+ const dest = ll ?? text;
43
+ if (!dest) return null;
44
+ const q = encodeURIComponent(dest);
45
+ if (provider === "apple") return `https://maps.apple.com/?daddr=${q}`;
46
+ if (provider === "geo") return ll ? `geo:${ll}?q=${ll}` : `geo:0,0?q=${q}`;
47
+ return `https://www.google.com/maps/dir/?api=1&destination=${q}`;
48
+ }
49
+ var toMin = (t) => {
50
+ const m = /^(\d{1,2}):(\d{2})$/.exec(t ?? "");
51
+ return m ? Number(m[1]) * 60 + Number(m[2]) : NaN;
52
+ };
53
+ var interval = (open, close) => {
54
+ const s = toMin(open);
55
+ let e = toMin(close);
56
+ if (Number.isNaN(s) || Number.isNaN(e)) return [];
57
+ if (e <= s) e += 1440;
58
+ return [[s, e]];
59
+ };
60
+ var dayIntervals = (h, day, date) => {
61
+ const x = h.exceptions?.find((e) => e.date === date);
62
+ if (x?.closed) return [];
63
+ if (x && x.open && x.close) return interval(x.open, x.close);
64
+ return h.weekly.filter((w) => w.day === day).flatMap((w) => interval(w.open, w.close));
65
+ };
66
+ function openStatus(hours, timeZone, now = /* @__PURE__ */ new Date()) {
67
+ const fmt = new Intl.DateTimeFormat("en-US", {
68
+ timeZone,
69
+ hourCycle: "h23",
70
+ year: "numeric",
71
+ month: "numeric",
72
+ day: "numeric",
73
+ hour: "numeric",
74
+ minute: "numeric"
75
+ });
76
+ const wall = (ms) => {
77
+ const p = {};
78
+ for (const { type, value } of fmt.formatToParts(ms)) p[type] = Number(value);
79
+ return Date.UTC(p.year, p.month - 1, p.day, (p.hour ?? 0) % 24, p.minute) / 6e4;
80
+ };
81
+ const nowMs = now.getTime();
82
+ const w = wall(nowMs);
83
+ const day0 = Math.floor(w / 1440) * 1440;
84
+ const offset = (ms) => wall(ms) - Math.floor(ms / 6e4);
85
+ const toInstant = (t) => {
86
+ const guess = (t - offset(nowMs)) * 6e4;
87
+ return new Date((t - offset(guess)) * 6e4).toISOString();
88
+ };
89
+ const all = [];
90
+ for (let d = -1; d <= 8; d++) {
91
+ const at = new Date((day0 + d * 1440) * 6e4);
92
+ const day = at.getUTCDay() || 7;
93
+ for (const [s, e] of dayIntervals(hours, day, at.toISOString().slice(0, 10))) all.push([day0 + d * 1440 + s, day0 + d * 1440 + e]);
94
+ }
95
+ all.sort((a, b) => a[0] - b[0]);
96
+ const merged = [];
97
+ for (const iv of all) {
98
+ const last = merged[merged.length - 1];
99
+ if (last && iv[0] <= last[1]) last[1] = Math.max(last[1], iv[1]);
100
+ else merged.push([iv[0], iv[1]]);
101
+ }
102
+ const current = merged.find(([s, e]) => s <= w && w < e);
103
+ if (current) return { open: true, closesAt: toInstant(current[1]), nextOpenAt: null };
104
+ const next = merged.find(([s]) => s > w);
105
+ return { open: false, closesAt: null, nextOpenAt: next ? toInstant(next[0]) : null };
106
+ }
107
+ var runs = (h) => {
108
+ const out = [];
109
+ for (let day = 1; day <= 7; day++) {
110
+ const iv = h.weekly.filter((x) => x.day === day).flatMap((x) => interval(x.open, x.close)).sort((a, b) => a[0] - b[0]);
111
+ if (!iv.length) continue;
112
+ const last = out[out.length - 1];
113
+ if (last && last.to === day - 1 && JSON.stringify(last.iv) === JSON.stringify(iv)) last.to = day;
114
+ else out.push({ from: day, to: day, iv });
115
+ }
116
+ return out;
117
+ };
118
+ var hm = (min) => [min && min % 1440 === 0 ? 24 : Math.floor(min / 60) % 24, String(min % 60).padStart(2, "0")];
119
+ function formatOpeningHours(hours, locale = "es-MX") {
120
+ const en = /^en/i.test(locale);
121
+ const name = (d) => {
122
+ const s = new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" }).format(Date.UTC(2024, 0, d)).replace(/\.$/, "");
123
+ return s.charAt(0).toUpperCase() + s.slice(1);
124
+ };
125
+ const time = (min) => {
126
+ const [h, m] = hm(min);
127
+ return en ? `${h % 12 || 12}:${m} ${h % 24 < 12 ? "AM" : "PM"}` : `${h}:${m}`;
128
+ };
129
+ return runs(hours).map((r) => `${name(r.from)}${r.from === r.to ? "" : "\u2013" + name(r.to)} ${r.iv.map(([s, e]) => `${time(s)}\u2013${time(e)}`).join(", ")}`).join(" \xB7 ");
130
+ }
131
+ function toSchemaOpeningHours(hours) {
132
+ const code = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
133
+ const t = (min) => {
134
+ const [h, m] = hm(min);
135
+ return `${String(h).padStart(2, "0")}:${m}`;
136
+ };
137
+ return runs(hours).flatMap(
138
+ (r) => r.iv.map(([s, e]) => `${code[r.from - 1]}${r.from === r.to ? "" : "-" + code[r.to - 1]} ${t(s)}-${t(e)}`)
139
+ );
140
+ }
141
+
142
+ export { directionsUrl, distanceKm, formatDistance, formatOpeningHours, isValidPoint, openStatus, parseLatLng, rankByDistance, toSchemaOpeningHours };
143
+ //# sourceMappingURL=chunk-45R47HYO.js.map
144
+ //# sourceMappingURL=chunk-45R47HYO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/geo.ts"],"names":[],"mappings":"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","file":"chunk-45R47HYO.js","sourcesContent":["/**\n * Geo helpers: pure TypeScript, no network and no DOM. Works in Node 20, edge\n * runtimes and browsers. Behaviour is pinned by `geo-vectors.json`, which the\n * Go side copies verbatim, so keep both implementations in sync.\n */\n\nexport type GeoPoint = { lat: number; lng: number }\n\n/** `(0, 0)` means \"unset\" on this platform, so it is never a valid point. */\ntype MaybePoint = { lat?: number | null; lng?: number | null } | null | undefined\n\nexport type OpeningHours = {\n /** `day`: 1 = Monday ... 7 = Sunday. Several entries per day are allowed. A `close` at or before `open` crosses midnight. */\n weekly: { day: number; open: string; close: string }[]\n /** Dated overrides of the weekly rule. `closed`, or both `open` and `close`. */\n exceptions?: { date: string; closed?: true; open?: string; close?: string; note?: string }[]\n}\n\nexport type OpenStatus = {\n open: boolean\n /** ISO instant (UTC) when the current interval ends; `null` when closed. */\n closesAt: string | null\n /** ISO instant (UTC) of the next opening; `null` when open or none within 8 days. */\n nextOpenAt: string | null\n}\n\nconst RAD = Math.PI / 180\n\n/** Great-circle distance in km (haversine, earth radius 6371.0088 km). */\nexport function distanceKm(a: GeoPoint, b: GeoPoint): number {\n const dLat = (b.lat - a.lat) * RAD\n const dLng = (b.lng - a.lng) * RAD\n const h = Math.sin(dLat / 2) ** 2 + Math.cos(a.lat * RAD) * Math.cos(b.lat * RAD) * Math.sin(dLng / 2) ** 2\n return 2 * 6371.0088 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h))\n}\n\n/** Finite, in range and not the `(0, 0)` \"unset\" pair. */\nexport function isValidPoint(p: MaybePoint): p is GeoPoint {\n const { lat, lng } = p ?? {}\n // Number.isFinite is false for null, undefined and non-numbers.\n return Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat!) <= 90 && Math.abs(lng!) <= 180 && !(lat === 0 && lng === 0)\n}\n\n/** `'19.43,-99.13'` (spaces allowed) to a point; `null` when malformed or invalid. */\nexport function parseLatLng(s: string | null | undefined): GeoPoint | null {\n const [a, b, ...rest] = (s ?? '').split(',')\n const p = !rest.length && a?.trim() && b?.trim() ? { lat: Number(a), lng: Number(b) } : null\n return isValidPoint(p) ? p : null\n}\n\nexport type RankOptions<T> = {\n /** Keep items at most this far away (inclusive). */\n radiusKm?: number\n /** Keep the first N after sorting. */\n limit?: number\n /** Defaults to reading `lat` / `lng` from the item. */\n getPoint?: (item: T) => MaybePoint\n}\n\n/**\n * Sorts by distance from `origin` (stable: ties keep input order). Items without\n * a valid point are EXCLUDED, not placed last, because they have no distance.\n */\nexport function rankByDistance<T>(\n items: readonly T[],\n origin: GeoPoint,\n { radiusKm, limit, getPoint = (i) => i as MaybePoint }: RankOptions<T> = {},\n): { item: T; distanceKm: number }[] {\n const out: { item: T; distanceKm: number }[] = []\n for (const item of items) {\n const p = getPoint(item)\n if (!isValidPoint(p)) continue\n const d = distanceKm(origin, p)\n if (radiusKm === undefined || d <= radiusKm) out.push({ item, distanceKm: d })\n }\n // Array.prototype.sort is stable (ES2019), so ties keep input order.\n out.sort((x, y) => x.distanceKm - y.distanceKm)\n return limit === undefined ? out : out.slice(0, Math.max(0, limit))\n}\n\n/** Under 1 km: meters rounded to 50 m. Otherwise km with one decimal. */\nexport function formatDistance(km: number, locale = 'es-MX'): string {\n const m = Math.round((km * 1000) / 50) * 50\n const [value, unit, digits] = m < 1000 ? [m, 'meter', 0] : [km, 'kilometer', 1]\n return new Intl.NumberFormat(locale, {\n style: 'unit', unit, minimumFractionDigits: digits, maximumFractionDigits: digits,\n }).format(value)\n}\n\n/** Destination for directions: coordinates when valid, else the address text. */\nexport function directionsUrl(\n location: { lat?: number | null; lng?: number | null; address?: string | null; city?: string | null },\n { provider = 'google' }: { provider?: 'google' | 'apple' | 'geo' } = {},\n): string | null {\n const ll = isValidPoint(location) ? `${location.lat},${location.lng}` : null\n const text = [location.address, location.city].filter(Boolean).join(', ')\n const dest = ll ?? text\n if (!dest) return null\n const q = encodeURIComponent(dest)\n if (provider === 'apple') return `https://maps.apple.com/?daddr=${q}`\n if (provider === 'geo') return ll ? `geo:${ll}?q=${ll}` : `geo:0,0?q=${q}`\n return `https://www.google.com/maps/dir/?api=1&destination=${q}`\n}\n\n// --- opening hours -------------------------------------------------------\n\nconst toMin = (t: string | undefined): number => {\n const m = /^(\\d{1,2}):(\\d{2})$/.exec(t ?? '')\n return m ? Number(m[1]) * 60 + Number(m[2]) : NaN\n}\n\ntype Iv = [number, number]\n\n/** Intervals in minutes; an end at or before the start crosses midnight. */\nconst interval = (open: string | undefined, close: string | undefined): Iv[] => {\n const s = toMin(open)\n let e = toMin(close)\n if (Number.isNaN(s) || Number.isNaN(e)) return []\n if (e <= s) e += 1440\n return [[s, e]]\n}\n\nconst dayIntervals = (h: OpeningHours, day: number, date: string): Iv[] => {\n const x = h.exceptions?.find((e) => e.date === date)\n if (x?.closed) return []\n if (x && x.open && x.close) return interval(x.open, x.close)\n return h.weekly.filter((w) => w.day === day).flatMap((w) => interval(w.open, w.close))\n}\n\n/**\n * Whether the place is open at `now` in the IANA `timeZone` (throws RangeError\n * for an unknown zone). Looks 8 days ahead for the next opening. Handles\n * intervals crossing midnight, several per day, dated exceptions and DST.\n */\nexport function openStatus(hours: OpeningHours, timeZone: string, now: Date = new Date()): OpenStatus {\n const fmt = new Intl.DateTimeFormat('en-US', {\n timeZone, hourCycle: 'h23', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric',\n })\n // Wall-clock minutes since the epoch as seen in `timeZone`.\n const wall = (ms: number): number => {\n const p: Record<string, number> = {}\n for (const { type, value } of fmt.formatToParts(ms)) p[type] = Number(value)\n return Date.UTC(p.year!, p.month! - 1, p.day!, (p.hour ?? 0) % 24, p.minute!) / 60000\n }\n const nowMs = now.getTime()\n const w = wall(nowMs)\n const day0 = Math.floor(w / 1440) * 1440\n // Offset from UTC, in minutes, at an instant.\n const offset = (ms: number): number => wall(ms) - Math.floor(ms / 60000)\n const toInstant = (t: number): string => {\n const guess = (t - offset(nowMs)) * 60000\n return new Date((t - offset(guess)) * 60000).toISOString()\n }\n\n const all: Iv[] = []\n for (let d = -1; d <= 8; d++) {\n const at = new Date((day0 + d * 1440) * 60000)\n const day = at.getUTCDay() || 7\n for (const [s, e] of dayIntervals(hours, day, at.toISOString().slice(0, 10))) all.push([day0 + d * 1440 + s, day0 + d * 1440 + e])\n }\n all.sort((a, b) => a[0] - b[0])\n const merged: Iv[] = []\n for (const iv of all) {\n const last = merged[merged.length - 1]\n if (last && iv[0] <= last[1]) last[1] = Math.max(last[1], iv[1])\n else merged.push([iv[0], iv[1]])\n }\n const current = merged.find(([s, e]) => s <= w && w < e)\n if (current) return { open: true, closesAt: toInstant(current[1]), nextOpenAt: null }\n const next = merged.find(([s]) => s > w)\n return { open: false, closesAt: null, nextOpenAt: next ? toInstant(next[0]) : null }\n}\n\ntype Run = { from: number; to: number; iv: Iv[] }\n\n/** Consecutive days (1..7) with identical intervals, in week order. */\nconst runs = (h: OpeningHours): Run[] => {\n const out: Run[] = []\n for (let day = 1; day <= 7; day++) {\n const iv = h.weekly.filter((x) => x.day === day).flatMap((x) => interval(x.open, x.close)).sort((a, b) => a[0] - b[0])\n if (!iv.length) continue\n const last = out[out.length - 1]\n if (last && last.to === day - 1 && JSON.stringify(last.iv) === JSON.stringify(iv)) last.to = day\n else out.push({ from: day, to: day, iv })\n }\n return out\n}\n\n/** Hour (24 for an end at midnight) and padded minutes of a minute-of-day value. */\nconst hm = (min: number): [number, string] => [min && min % 1440 === 0 ? 24 : Math.floor(min / 60) % 24, String(min % 60).padStart(2, '0')]\n\n/** Compact text, e.g. `Lun–Vie 9:00–19:00 · Sáb 9:00–14:00` (24 h) or `Mon–Fri 9:00 AM–7:00 PM` (en). */\nexport function formatOpeningHours(hours: OpeningHours, locale = 'es-MX'): string {\n const en = /^en/i.test(locale)\n const name = (d: number): string => {\n const s = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' }).format(Date.UTC(2024, 0, d)).replace(/\\.$/, '')\n return s.charAt(0).toUpperCase() + s.slice(1)\n }\n const time = (min: number): string => {\n const [h, m] = hm(min)\n return en ? `${h % 12 || 12}:${m} ${h % 24 < 12 ? 'AM' : 'PM'}` : `${h}:${m}`\n }\n return runs(hours)\n .map((r) => `${name(r.from)}${r.from === r.to ? '' : '–' + name(r.to)} ${r.iv.map(([s, e]) => `${time(s)}–${time(e)}`).join(', ')}`)\n .join(' · ')\n}\n\n/** schema.org `openingHours` strings, one per interval, e.g. `Mo-Fr 09:00-19:00`. */\nexport function toSchemaOpeningHours(hours: OpeningHours): string[] {\n const code = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']\n const t = (min: number): string => { const [h, m] = hm(min); return `${String(h).padStart(2, '0')}:${m}` }\n return runs(hours).flatMap((r) =>\n r.iv.map(([s, e]) => `${code[r.from - 1]}${r.from === r.to ? '' : '-' + code[r.to - 1]} ${t(s)}-${t(e)}`),\n )\n}\n"]}
package/dist/geo.cjs ADDED
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ // src/geo.ts
4
+ var RAD = Math.PI / 180;
5
+ function distanceKm(a, b) {
6
+ const dLat = (b.lat - a.lat) * RAD;
7
+ const dLng = (b.lng - a.lng) * RAD;
8
+ const h = Math.sin(dLat / 2) ** 2 + Math.cos(a.lat * RAD) * Math.cos(b.lat * RAD) * Math.sin(dLng / 2) ** 2;
9
+ return 2 * 6371.0088 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
10
+ }
11
+ function isValidPoint(p) {
12
+ const { lat, lng } = p ?? {};
13
+ return Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat) <= 90 && Math.abs(lng) <= 180 && !(lat === 0 && lng === 0);
14
+ }
15
+ function parseLatLng(s) {
16
+ const [a, b, ...rest] = (s ?? "").split(",");
17
+ const p = !rest.length && a?.trim() && b?.trim() ? { lat: Number(a), lng: Number(b) } : null;
18
+ return isValidPoint(p) ? p : null;
19
+ }
20
+ function rankByDistance(items, origin, { radiusKm, limit, getPoint = (i) => i } = {}) {
21
+ const out = [];
22
+ for (const item of items) {
23
+ const p = getPoint(item);
24
+ if (!isValidPoint(p)) continue;
25
+ const d = distanceKm(origin, p);
26
+ if (radiusKm === void 0 || d <= radiusKm) out.push({ item, distanceKm: d });
27
+ }
28
+ out.sort((x, y) => x.distanceKm - y.distanceKm);
29
+ return limit === void 0 ? out : out.slice(0, Math.max(0, limit));
30
+ }
31
+ function formatDistance(km, locale = "es-MX") {
32
+ const m = Math.round(km * 1e3 / 50) * 50;
33
+ const [value, unit, digits] = m < 1e3 ? [m, "meter", 0] : [km, "kilometer", 1];
34
+ return new Intl.NumberFormat(locale, {
35
+ style: "unit",
36
+ unit,
37
+ minimumFractionDigits: digits,
38
+ maximumFractionDigits: digits
39
+ }).format(value);
40
+ }
41
+ function directionsUrl(location, { provider = "google" } = {}) {
42
+ const ll = isValidPoint(location) ? `${location.lat},${location.lng}` : null;
43
+ const text = [location.address, location.city].filter(Boolean).join(", ");
44
+ const dest = ll ?? text;
45
+ if (!dest) return null;
46
+ const q = encodeURIComponent(dest);
47
+ if (provider === "apple") return `https://maps.apple.com/?daddr=${q}`;
48
+ if (provider === "geo") return ll ? `geo:${ll}?q=${ll}` : `geo:0,0?q=${q}`;
49
+ return `https://www.google.com/maps/dir/?api=1&destination=${q}`;
50
+ }
51
+ var toMin = (t) => {
52
+ const m = /^(\d{1,2}):(\d{2})$/.exec(t ?? "");
53
+ return m ? Number(m[1]) * 60 + Number(m[2]) : NaN;
54
+ };
55
+ var interval = (open, close) => {
56
+ const s = toMin(open);
57
+ let e = toMin(close);
58
+ if (Number.isNaN(s) || Number.isNaN(e)) return [];
59
+ if (e <= s) e += 1440;
60
+ return [[s, e]];
61
+ };
62
+ var dayIntervals = (h, day, date) => {
63
+ const x = h.exceptions?.find((e) => e.date === date);
64
+ if (x?.closed) return [];
65
+ if (x && x.open && x.close) return interval(x.open, x.close);
66
+ return h.weekly.filter((w) => w.day === day).flatMap((w) => interval(w.open, w.close));
67
+ };
68
+ function openStatus(hours, timeZone, now = /* @__PURE__ */ new Date()) {
69
+ const fmt = new Intl.DateTimeFormat("en-US", {
70
+ timeZone,
71
+ hourCycle: "h23",
72
+ year: "numeric",
73
+ month: "numeric",
74
+ day: "numeric",
75
+ hour: "numeric",
76
+ minute: "numeric"
77
+ });
78
+ const wall = (ms) => {
79
+ const p = {};
80
+ for (const { type, value } of fmt.formatToParts(ms)) p[type] = Number(value);
81
+ return Date.UTC(p.year, p.month - 1, p.day, (p.hour ?? 0) % 24, p.minute) / 6e4;
82
+ };
83
+ const nowMs = now.getTime();
84
+ const w = wall(nowMs);
85
+ const day0 = Math.floor(w / 1440) * 1440;
86
+ const offset = (ms) => wall(ms) - Math.floor(ms / 6e4);
87
+ const toInstant = (t) => {
88
+ const guess = (t - offset(nowMs)) * 6e4;
89
+ return new Date((t - offset(guess)) * 6e4).toISOString();
90
+ };
91
+ const all = [];
92
+ for (let d = -1; d <= 8; d++) {
93
+ const at = new Date((day0 + d * 1440) * 6e4);
94
+ const day = at.getUTCDay() || 7;
95
+ for (const [s, e] of dayIntervals(hours, day, at.toISOString().slice(0, 10))) all.push([day0 + d * 1440 + s, day0 + d * 1440 + e]);
96
+ }
97
+ all.sort((a, b) => a[0] - b[0]);
98
+ const merged = [];
99
+ for (const iv of all) {
100
+ const last = merged[merged.length - 1];
101
+ if (last && iv[0] <= last[1]) last[1] = Math.max(last[1], iv[1]);
102
+ else merged.push([iv[0], iv[1]]);
103
+ }
104
+ const current = merged.find(([s, e]) => s <= w && w < e);
105
+ if (current) return { open: true, closesAt: toInstant(current[1]), nextOpenAt: null };
106
+ const next = merged.find(([s]) => s > w);
107
+ return { open: false, closesAt: null, nextOpenAt: next ? toInstant(next[0]) : null };
108
+ }
109
+ var runs = (h) => {
110
+ const out = [];
111
+ for (let day = 1; day <= 7; day++) {
112
+ const iv = h.weekly.filter((x) => x.day === day).flatMap((x) => interval(x.open, x.close)).sort((a, b) => a[0] - b[0]);
113
+ if (!iv.length) continue;
114
+ const last = out[out.length - 1];
115
+ if (last && last.to === day - 1 && JSON.stringify(last.iv) === JSON.stringify(iv)) last.to = day;
116
+ else out.push({ from: day, to: day, iv });
117
+ }
118
+ return out;
119
+ };
120
+ var hm = (min) => [min && min % 1440 === 0 ? 24 : Math.floor(min / 60) % 24, String(min % 60).padStart(2, "0")];
121
+ function formatOpeningHours(hours, locale = "es-MX") {
122
+ const en = /^en/i.test(locale);
123
+ const name = (d) => {
124
+ const s = new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" }).format(Date.UTC(2024, 0, d)).replace(/\.$/, "");
125
+ return s.charAt(0).toUpperCase() + s.slice(1);
126
+ };
127
+ const time = (min) => {
128
+ const [h, m] = hm(min);
129
+ return en ? `${h % 12 || 12}:${m} ${h % 24 < 12 ? "AM" : "PM"}` : `${h}:${m}`;
130
+ };
131
+ return runs(hours).map((r) => `${name(r.from)}${r.from === r.to ? "" : "\u2013" + name(r.to)} ${r.iv.map(([s, e]) => `${time(s)}\u2013${time(e)}`).join(", ")}`).join(" \xB7 ");
132
+ }
133
+ function toSchemaOpeningHours(hours) {
134
+ const code = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
135
+ const t = (min) => {
136
+ const [h, m] = hm(min);
137
+ return `${String(h).padStart(2, "0")}:${m}`;
138
+ };
139
+ return runs(hours).flatMap(
140
+ (r) => r.iv.map(([s, e]) => `${code[r.from - 1]}${r.from === r.to ? "" : "-" + code[r.to - 1]} ${t(s)}-${t(e)}`)
141
+ );
142
+ }
143
+
144
+ exports.directionsUrl = directionsUrl;
145
+ exports.distanceKm = distanceKm;
146
+ exports.formatDistance = formatDistance;
147
+ exports.formatOpeningHours = formatOpeningHours;
148
+ exports.isValidPoint = isValidPoint;
149
+ exports.openStatus = openStatus;
150
+ exports.parseLatLng = parseLatLng;
151
+ exports.rankByDistance = rankByDistance;
152
+ exports.toSchemaOpeningHours = toSchemaOpeningHours;
153
+ //# sourceMappingURL=geo.cjs.map
154
+ //# sourceMappingURL=geo.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/geo.ts"],"names":[],"mappings":"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","file":"geo.cjs","sourcesContent":["/**\n * Geo helpers: pure TypeScript, no network and no DOM. Works in Node 20, edge\n * runtimes and browsers. Behaviour is pinned by `geo-vectors.json`, which the\n * Go side copies verbatim, so keep both implementations in sync.\n */\n\nexport type GeoPoint = { lat: number; lng: number }\n\n/** `(0, 0)` means \"unset\" on this platform, so it is never a valid point. */\ntype MaybePoint = { lat?: number | null; lng?: number | null } | null | undefined\n\nexport type OpeningHours = {\n /** `day`: 1 = Monday ... 7 = Sunday. Several entries per day are allowed. A `close` at or before `open` crosses midnight. */\n weekly: { day: number; open: string; close: string }[]\n /** Dated overrides of the weekly rule. `closed`, or both `open` and `close`. */\n exceptions?: { date: string; closed?: true; open?: string; close?: string; note?: string }[]\n}\n\nexport type OpenStatus = {\n open: boolean\n /** ISO instant (UTC) when the current interval ends; `null` when closed. */\n closesAt: string | null\n /** ISO instant (UTC) of the next opening; `null` when open or none within 8 days. */\n nextOpenAt: string | null\n}\n\nconst RAD = Math.PI / 180\n\n/** Great-circle distance in km (haversine, earth radius 6371.0088 km). */\nexport function distanceKm(a: GeoPoint, b: GeoPoint): number {\n const dLat = (b.lat - a.lat) * RAD\n const dLng = (b.lng - a.lng) * RAD\n const h = Math.sin(dLat / 2) ** 2 + Math.cos(a.lat * RAD) * Math.cos(b.lat * RAD) * Math.sin(dLng / 2) ** 2\n return 2 * 6371.0088 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h))\n}\n\n/** Finite, in range and not the `(0, 0)` \"unset\" pair. */\nexport function isValidPoint(p: MaybePoint): p is GeoPoint {\n const { lat, lng } = p ?? {}\n // Number.isFinite is false for null, undefined and non-numbers.\n return Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat!) <= 90 && Math.abs(lng!) <= 180 && !(lat === 0 && lng === 0)\n}\n\n/** `'19.43,-99.13'` (spaces allowed) to a point; `null` when malformed or invalid. */\nexport function parseLatLng(s: string | null | undefined): GeoPoint | null {\n const [a, b, ...rest] = (s ?? '').split(',')\n const p = !rest.length && a?.trim() && b?.trim() ? { lat: Number(a), lng: Number(b) } : null\n return isValidPoint(p) ? p : null\n}\n\nexport type RankOptions<T> = {\n /** Keep items at most this far away (inclusive). */\n radiusKm?: number\n /** Keep the first N after sorting. */\n limit?: number\n /** Defaults to reading `lat` / `lng` from the item. */\n getPoint?: (item: T) => MaybePoint\n}\n\n/**\n * Sorts by distance from `origin` (stable: ties keep input order). Items without\n * a valid point are EXCLUDED, not placed last, because they have no distance.\n */\nexport function rankByDistance<T>(\n items: readonly T[],\n origin: GeoPoint,\n { radiusKm, limit, getPoint = (i) => i as MaybePoint }: RankOptions<T> = {},\n): { item: T; distanceKm: number }[] {\n const out: { item: T; distanceKm: number }[] = []\n for (const item of items) {\n const p = getPoint(item)\n if (!isValidPoint(p)) continue\n const d = distanceKm(origin, p)\n if (radiusKm === undefined || d <= radiusKm) out.push({ item, distanceKm: d })\n }\n // Array.prototype.sort is stable (ES2019), so ties keep input order.\n out.sort((x, y) => x.distanceKm - y.distanceKm)\n return limit === undefined ? out : out.slice(0, Math.max(0, limit))\n}\n\n/** Under 1 km: meters rounded to 50 m. Otherwise km with one decimal. */\nexport function formatDistance(km: number, locale = 'es-MX'): string {\n const m = Math.round((km * 1000) / 50) * 50\n const [value, unit, digits] = m < 1000 ? [m, 'meter', 0] : [km, 'kilometer', 1]\n return new Intl.NumberFormat(locale, {\n style: 'unit', unit, minimumFractionDigits: digits, maximumFractionDigits: digits,\n }).format(value)\n}\n\n/** Destination for directions: coordinates when valid, else the address text. */\nexport function directionsUrl(\n location: { lat?: number | null; lng?: number | null; address?: string | null; city?: string | null },\n { provider = 'google' }: { provider?: 'google' | 'apple' | 'geo' } = {},\n): string | null {\n const ll = isValidPoint(location) ? `${location.lat},${location.lng}` : null\n const text = [location.address, location.city].filter(Boolean).join(', ')\n const dest = ll ?? text\n if (!dest) return null\n const q = encodeURIComponent(dest)\n if (provider === 'apple') return `https://maps.apple.com/?daddr=${q}`\n if (provider === 'geo') return ll ? `geo:${ll}?q=${ll}` : `geo:0,0?q=${q}`\n return `https://www.google.com/maps/dir/?api=1&destination=${q}`\n}\n\n// --- opening hours -------------------------------------------------------\n\nconst toMin = (t: string | undefined): number => {\n const m = /^(\\d{1,2}):(\\d{2})$/.exec(t ?? '')\n return m ? Number(m[1]) * 60 + Number(m[2]) : NaN\n}\n\ntype Iv = [number, number]\n\n/** Intervals in minutes; an end at or before the start crosses midnight. */\nconst interval = (open: string | undefined, close: string | undefined): Iv[] => {\n const s = toMin(open)\n let e = toMin(close)\n if (Number.isNaN(s) || Number.isNaN(e)) return []\n if (e <= s) e += 1440\n return [[s, e]]\n}\n\nconst dayIntervals = (h: OpeningHours, day: number, date: string): Iv[] => {\n const x = h.exceptions?.find((e) => e.date === date)\n if (x?.closed) return []\n if (x && x.open && x.close) return interval(x.open, x.close)\n return h.weekly.filter((w) => w.day === day).flatMap((w) => interval(w.open, w.close))\n}\n\n/**\n * Whether the place is open at `now` in the IANA `timeZone` (throws RangeError\n * for an unknown zone). Looks 8 days ahead for the next opening. Handles\n * intervals crossing midnight, several per day, dated exceptions and DST.\n */\nexport function openStatus(hours: OpeningHours, timeZone: string, now: Date = new Date()): OpenStatus {\n const fmt = new Intl.DateTimeFormat('en-US', {\n timeZone, hourCycle: 'h23', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric',\n })\n // Wall-clock minutes since the epoch as seen in `timeZone`.\n const wall = (ms: number): number => {\n const p: Record<string, number> = {}\n for (const { type, value } of fmt.formatToParts(ms)) p[type] = Number(value)\n return Date.UTC(p.year!, p.month! - 1, p.day!, (p.hour ?? 0) % 24, p.minute!) / 60000\n }\n const nowMs = now.getTime()\n const w = wall(nowMs)\n const day0 = Math.floor(w / 1440) * 1440\n // Offset from UTC, in minutes, at an instant.\n const offset = (ms: number): number => wall(ms) - Math.floor(ms / 60000)\n const toInstant = (t: number): string => {\n const guess = (t - offset(nowMs)) * 60000\n return new Date((t - offset(guess)) * 60000).toISOString()\n }\n\n const all: Iv[] = []\n for (let d = -1; d <= 8; d++) {\n const at = new Date((day0 + d * 1440) * 60000)\n const day = at.getUTCDay() || 7\n for (const [s, e] of dayIntervals(hours, day, at.toISOString().slice(0, 10))) all.push([day0 + d * 1440 + s, day0 + d * 1440 + e])\n }\n all.sort((a, b) => a[0] - b[0])\n const merged: Iv[] = []\n for (const iv of all) {\n const last = merged[merged.length - 1]\n if (last && iv[0] <= last[1]) last[1] = Math.max(last[1], iv[1])\n else merged.push([iv[0], iv[1]])\n }\n const current = merged.find(([s, e]) => s <= w && w < e)\n if (current) return { open: true, closesAt: toInstant(current[1]), nextOpenAt: null }\n const next = merged.find(([s]) => s > w)\n return { open: false, closesAt: null, nextOpenAt: next ? toInstant(next[0]) : null }\n}\n\ntype Run = { from: number; to: number; iv: Iv[] }\n\n/** Consecutive days (1..7) with identical intervals, in week order. */\nconst runs = (h: OpeningHours): Run[] => {\n const out: Run[] = []\n for (let day = 1; day <= 7; day++) {\n const iv = h.weekly.filter((x) => x.day === day).flatMap((x) => interval(x.open, x.close)).sort((a, b) => a[0] - b[0])\n if (!iv.length) continue\n const last = out[out.length - 1]\n if (last && last.to === day - 1 && JSON.stringify(last.iv) === JSON.stringify(iv)) last.to = day\n else out.push({ from: day, to: day, iv })\n }\n return out\n}\n\n/** Hour (24 for an end at midnight) and padded minutes of a minute-of-day value. */\nconst hm = (min: number): [number, string] => [min && min % 1440 === 0 ? 24 : Math.floor(min / 60) % 24, String(min % 60).padStart(2, '0')]\n\n/** Compact text, e.g. `Lun–Vie 9:00–19:00 · Sáb 9:00–14:00` (24 h) or `Mon–Fri 9:00 AM–7:00 PM` (en). */\nexport function formatOpeningHours(hours: OpeningHours, locale = 'es-MX'): string {\n const en = /^en/i.test(locale)\n const name = (d: number): string => {\n const s = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' }).format(Date.UTC(2024, 0, d)).replace(/\\.$/, '')\n return s.charAt(0).toUpperCase() + s.slice(1)\n }\n const time = (min: number): string => {\n const [h, m] = hm(min)\n return en ? `${h % 12 || 12}:${m} ${h % 24 < 12 ? 'AM' : 'PM'}` : `${h}:${m}`\n }\n return runs(hours)\n .map((r) => `${name(r.from)}${r.from === r.to ? '' : '–' + name(r.to)} ${r.iv.map(([s, e]) => `${time(s)}–${time(e)}`).join(', ')}`)\n .join(' · ')\n}\n\n/** schema.org `openingHours` strings, one per interval, e.g. `Mo-Fr 09:00-19:00`. */\nexport function toSchemaOpeningHours(hours: OpeningHours): string[] {\n const code = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']\n const t = (min: number): string => { const [h, m] = hm(min); return `${String(h).padStart(2, '0')}:${m}` }\n return runs(hours).flatMap((r) =>\n r.iv.map(([s, e]) => `${code[r.from - 1]}${r.from === r.to ? '' : '-' + code[r.to - 1]} ${t(s)}-${t(e)}`),\n )\n}\n"]}
package/dist/geo.d.cts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Geo helpers: pure TypeScript, no network and no DOM. Works in Node 20, edge
3
+ * runtimes and browsers. Behaviour is pinned by `geo-vectors.json`, which the
4
+ * Go side copies verbatim, so keep both implementations in sync.
5
+ */
6
+ type GeoPoint = {
7
+ lat: number;
8
+ lng: number;
9
+ };
10
+ /** `(0, 0)` means "unset" on this platform, so it is never a valid point. */
11
+ type MaybePoint = {
12
+ lat?: number | null;
13
+ lng?: number | null;
14
+ } | null | undefined;
15
+ type OpeningHours = {
16
+ /** `day`: 1 = Monday ... 7 = Sunday. Several entries per day are allowed. A `close` at or before `open` crosses midnight. */
17
+ weekly: {
18
+ day: number;
19
+ open: string;
20
+ close: string;
21
+ }[];
22
+ /** Dated overrides of the weekly rule. `closed`, or both `open` and `close`. */
23
+ exceptions?: {
24
+ date: string;
25
+ closed?: true;
26
+ open?: string;
27
+ close?: string;
28
+ note?: string;
29
+ }[];
30
+ };
31
+ type OpenStatus = {
32
+ open: boolean;
33
+ /** ISO instant (UTC) when the current interval ends; `null` when closed. */
34
+ closesAt: string | null;
35
+ /** ISO instant (UTC) of the next opening; `null` when open or none within 8 days. */
36
+ nextOpenAt: string | null;
37
+ };
38
+ /** Great-circle distance in km (haversine, earth radius 6371.0088 km). */
39
+ declare function distanceKm(a: GeoPoint, b: GeoPoint): number;
40
+ /** Finite, in range and not the `(0, 0)` "unset" pair. */
41
+ declare function isValidPoint(p: MaybePoint): p is GeoPoint;
42
+ /** `'19.43,-99.13'` (spaces allowed) to a point; `null` when malformed or invalid. */
43
+ declare function parseLatLng(s: string | null | undefined): GeoPoint | null;
44
+ type RankOptions<T> = {
45
+ /** Keep items at most this far away (inclusive). */
46
+ radiusKm?: number;
47
+ /** Keep the first N after sorting. */
48
+ limit?: number;
49
+ /** Defaults to reading `lat` / `lng` from the item. */
50
+ getPoint?: (item: T) => MaybePoint;
51
+ };
52
+ /**
53
+ * Sorts by distance from `origin` (stable: ties keep input order). Items without
54
+ * a valid point are EXCLUDED, not placed last, because they have no distance.
55
+ */
56
+ declare function rankByDistance<T>(items: readonly T[], origin: GeoPoint, { radiusKm, limit, getPoint }?: RankOptions<T>): {
57
+ item: T;
58
+ distanceKm: number;
59
+ }[];
60
+ /** Under 1 km: meters rounded to 50 m. Otherwise km with one decimal. */
61
+ declare function formatDistance(km: number, locale?: string): string;
62
+ /** Destination for directions: coordinates when valid, else the address text. */
63
+ declare function directionsUrl(location: {
64
+ lat?: number | null;
65
+ lng?: number | null;
66
+ address?: string | null;
67
+ city?: string | null;
68
+ }, { provider }?: {
69
+ provider?: 'google' | 'apple' | 'geo';
70
+ }): string | null;
71
+ /**
72
+ * Whether the place is open at `now` in the IANA `timeZone` (throws RangeError
73
+ * for an unknown zone). Looks 8 days ahead for the next opening. Handles
74
+ * intervals crossing midnight, several per day, dated exceptions and DST.
75
+ */
76
+ declare function openStatus(hours: OpeningHours, timeZone: string, now?: Date): OpenStatus;
77
+ /** Compact text, e.g. `Lun–Vie 9:00–19:00 · Sáb 9:00–14:00` (24 h) or `Mon–Fri 9:00 AM–7:00 PM` (en). */
78
+ declare function formatOpeningHours(hours: OpeningHours, locale?: string): string;
79
+ /** schema.org `openingHours` strings, one per interval, e.g. `Mo-Fr 09:00-19:00`. */
80
+ declare function toSchemaOpeningHours(hours: OpeningHours): string[];
81
+
82
+ export { type GeoPoint, type OpenStatus, type OpeningHours, type RankOptions, directionsUrl, distanceKm, formatDistance, formatOpeningHours, isValidPoint, openStatus, parseLatLng, rankByDistance, toSchemaOpeningHours };
package/dist/geo.d.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Geo helpers: pure TypeScript, no network and no DOM. Works in Node 20, edge
3
+ * runtimes and browsers. Behaviour is pinned by `geo-vectors.json`, which the
4
+ * Go side copies verbatim, so keep both implementations in sync.
5
+ */
6
+ type GeoPoint = {
7
+ lat: number;
8
+ lng: number;
9
+ };
10
+ /** `(0, 0)` means "unset" on this platform, so it is never a valid point. */
11
+ type MaybePoint = {
12
+ lat?: number | null;
13
+ lng?: number | null;
14
+ } | null | undefined;
15
+ type OpeningHours = {
16
+ /** `day`: 1 = Monday ... 7 = Sunday. Several entries per day are allowed. A `close` at or before `open` crosses midnight. */
17
+ weekly: {
18
+ day: number;
19
+ open: string;
20
+ close: string;
21
+ }[];
22
+ /** Dated overrides of the weekly rule. `closed`, or both `open` and `close`. */
23
+ exceptions?: {
24
+ date: string;
25
+ closed?: true;
26
+ open?: string;
27
+ close?: string;
28
+ note?: string;
29
+ }[];
30
+ };
31
+ type OpenStatus = {
32
+ open: boolean;
33
+ /** ISO instant (UTC) when the current interval ends; `null` when closed. */
34
+ closesAt: string | null;
35
+ /** ISO instant (UTC) of the next opening; `null` when open or none within 8 days. */
36
+ nextOpenAt: string | null;
37
+ };
38
+ /** Great-circle distance in km (haversine, earth radius 6371.0088 km). */
39
+ declare function distanceKm(a: GeoPoint, b: GeoPoint): number;
40
+ /** Finite, in range and not the `(0, 0)` "unset" pair. */
41
+ declare function isValidPoint(p: MaybePoint): p is GeoPoint;
42
+ /** `'19.43,-99.13'` (spaces allowed) to a point; `null` when malformed or invalid. */
43
+ declare function parseLatLng(s: string | null | undefined): GeoPoint | null;
44
+ type RankOptions<T> = {
45
+ /** Keep items at most this far away (inclusive). */
46
+ radiusKm?: number;
47
+ /** Keep the first N after sorting. */
48
+ limit?: number;
49
+ /** Defaults to reading `lat` / `lng` from the item. */
50
+ getPoint?: (item: T) => MaybePoint;
51
+ };
52
+ /**
53
+ * Sorts by distance from `origin` (stable: ties keep input order). Items without
54
+ * a valid point are EXCLUDED, not placed last, because they have no distance.
55
+ */
56
+ declare function rankByDistance<T>(items: readonly T[], origin: GeoPoint, { radiusKm, limit, getPoint }?: RankOptions<T>): {
57
+ item: T;
58
+ distanceKm: number;
59
+ }[];
60
+ /** Under 1 km: meters rounded to 50 m. Otherwise km with one decimal. */
61
+ declare function formatDistance(km: number, locale?: string): string;
62
+ /** Destination for directions: coordinates when valid, else the address text. */
63
+ declare function directionsUrl(location: {
64
+ lat?: number | null;
65
+ lng?: number | null;
66
+ address?: string | null;
67
+ city?: string | null;
68
+ }, { provider }?: {
69
+ provider?: 'google' | 'apple' | 'geo';
70
+ }): string | null;
71
+ /**
72
+ * Whether the place is open at `now` in the IANA `timeZone` (throws RangeError
73
+ * for an unknown zone). Looks 8 days ahead for the next opening. Handles
74
+ * intervals crossing midnight, several per day, dated exceptions and DST.
75
+ */
76
+ declare function openStatus(hours: OpeningHours, timeZone: string, now?: Date): OpenStatus;
77
+ /** Compact text, e.g. `Lun–Vie 9:00–19:00 · Sáb 9:00–14:00` (24 h) or `Mon–Fri 9:00 AM–7:00 PM` (en). */
78
+ declare function formatOpeningHours(hours: OpeningHours, locale?: string): string;
79
+ /** schema.org `openingHours` strings, one per interval, e.g. `Mo-Fr 09:00-19:00`. */
80
+ declare function toSchemaOpeningHours(hours: OpeningHours): string[];
81
+
82
+ export { type GeoPoint, type OpenStatus, type OpeningHours, type RankOptions, directionsUrl, distanceKm, formatDistance, formatOpeningHours, isValidPoint, openStatus, parseLatLng, rankByDistance, toSchemaOpeningHours };
package/dist/geo.js ADDED
@@ -0,0 +1,3 @@
1
+ export { directionsUrl, distanceKm, formatDistance, formatOpeningHours, isValidPoint, openStatus, parseLatLng, rankByDistance, toSchemaOpeningHours } from './chunk-45R47HYO.js';
2
+ //# sourceMappingURL=geo.js.map
3
+ //# sourceMappingURL=geo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"geo.js"}
package/dist/index.cjs CHANGED
@@ -33,6 +33,10 @@ var STATUS_CODES = {
33
33
  function defaultCodeForStatus(status) {
34
34
  return STATUS_CODES[status] ?? (status >= 500 ? "server_error" : "invalid_response");
35
35
  }
36
+ function isValidPoint(p) {
37
+ const { lat, lng } = p ?? {};
38
+ return Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat) <= 90 && Math.abs(lng) <= 180 && !(lat === 0 && lng === 0);
39
+ }
36
40
 
37
41
  // src/money.ts
38
42
  function parseMoney(money) {
@@ -254,6 +258,22 @@ function createStorefrontClient(config) {
254
258
  /** `null` when the collection does not exist. */
255
259
  get: (handle, options) => getOrNull(`/storefront/collections/${enc(handle)}`, void 0, options)
256
260
  },
261
+ locations: {
262
+ /** Branches of the shop. With `postal_code` they come ordered by distance (`distance_km`). */
263
+ list: (params, options) => get("/storefront/locations", params, options),
264
+ /** `null` when the branch does not exist or is not visible. */
265
+ get: (id, options) => getOrNull(`/storefront/locations/${enc(id)}`, void 0, options),
266
+ /**
267
+ * Branches ordered by distance to a point, closest first. The server rounds the coordinates (0.01°, ~1 km)
268
+ * and never stores them; this client does not log or keep them either. The answer is `private, no-store`
269
+ * (it depends on the buyer's position), so do not put it in a shared cache. Note `onRequest` hooks and
270
+ * `fetch` wrappers do see the URL: do not log it. Throws `TypeError` for an invalid point.
271
+ */
272
+ nearest: (point, options) => {
273
+ if (!isValidPoint(point)) throw new TypeError("locations.nearest: lat/lng must be a valid point");
274
+ return get("/storefront/locations/nearest", { lat: point.lat, lng: point.lng }, { cache: "no-store", ...options });
275
+ }
276
+ },
257
277
  stock: {
258
278
  /** Stock for up to 250 SKUs. */
259
279
  get: (skus, options) => get("/storefront/stock", { sku: skus }, options)