rendezvous-kit 1.21.4 → 1.21.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 forgesworn
3
+ Copyright (c) 2026 TheCryptoDonkey
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # rendezvous-kit
2
2
 
3
+ **Nostr:** [`npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2`](https://njump.me/npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2)
4
+
3
5
  **Find fair meeting points for N people — isochrone intersection, venue search, and fairness scoring.**
4
6
 
5
7
  [![npm](https://img.shields.io/npm/v/rendezvous-kit)](https://www.npmjs.com/package/rendezvous-kit)
@@ -92,12 +94,12 @@ for (const s of suggestions) {
92
94
 
93
95
  ## Engine Support
94
96
 
95
- | Engine | Isochrone | Route Matrix | Auth |
96
- |--------|:---------:|:------------:|------|
97
- | Valhalla | Yes | Yes | None (self-hosted) |
98
- | OpenRouteService | Yes | Yes | API key |
99
- | GraphHopper | Yes | Yes | API key (optional) |
100
- | OSRM | No | Yes | None (self-hosted) |
97
+ | Engine | Isochrone | Route Matrix | Route | Auth |
98
+ |--------|:---------:|:------------:|:-----:|------|
99
+ | Valhalla | Yes | Yes | Yes | None (self-hosted) |
100
+ | OpenRouteService | Yes | Yes | No | API key |
101
+ | GraphHopper | Yes | Yes | No | API key (optional) |
102
+ | OSRM | No | Yes | No | None (self-hosted) |
101
103
 
102
104
  OSRM does not support isochrone computation — use it only when you need a fast route matrix and are supplying your own intersection polygon.
103
105
 
@@ -152,14 +154,17 @@ OSRM does not support isochrone computation — use it only when you need a fast
152
154
  | `GeoJSONPolygon` | Standard GeoJSON polygon geometry |
153
155
  | `TransportMode` | `'drive' \| 'cycle' \| 'walk' \| 'public_transit'` |
154
156
  | `FairnessStrategy` | `'min_max' \| 'min_total' \| 'min_variance'` |
155
- | `VenueType` | `'park' \| 'cafe' \| 'restaurant' \| 'service_station' \| 'library' \| 'pub' \| 'playground' \| 'community_centre' \| string` |
156
- | `RoutingEngine` | Interface — `computeIsochrone` + `computeRouteMatrix` |
157
+ | `VenueType` | `'park' \| 'cafe' \| 'restaurant' \| 'service_station' \| 'library' \| 'pub' \| 'playground' \| 'community_centre' \| 'bar' \| 'fast_food' \| 'garden' \| 'theatre' \| 'arts_centre' \| 'fitness_centre' \| 'sports_centre' \| 'escape_game' \| 'swimming_pool' \| string` |
158
+ | `RoutingEngine` | Interface — `computeIsochrone` + `computeRouteMatrix` + `computeRoute` |
157
159
  | `Isochrone` | `{ origin, mode, timeMinutes, polygon }` |
158
160
  | `MatrixEntry` | `{ originIndex, destinationIndex, durationMinutes, distanceKm }` |
159
161
  | `RouteMatrix` | `{ origins, destinations, entries }` |
160
162
  | `Venue` | `{ name, lat, lon, venueType, osmId? }` |
161
- | `RendezvousOptions` | `{ participants, mode, maxTimeMinutes, venueTypes, fairness?, limit? }` |
162
- | `RendezvousSuggestion` | `{ venue, travelTimes, fairnessScore }` |
163
+ | `RendezvousOptions` | `{ participants, mode, maxTimeMinutes, venueTypes, fairness?, limit?, strategy? }` |
164
+ | `RendezvousSuggestion` | `{ venue, travelTimes, fairnessScore, metadata? }` |
165
+ | `RouteGeometry` | `{ origin, destination, mode, durationMinutes, distanceKm, geometry, legs? }` |
166
+ | `RouteLeg` | `{ instruction, distanceKm, durationMinutes, type?, streetNames?, ... }` |
167
+ | `GeoJSONLineString` | `{ type: 'LineString', coordinates: number[][] }` |
163
168
  | `BBox` | `{ minLon, minLat, maxLon, maxLat }` |
164
169
  | `Coordinate` | `{ lat, lon }` |
165
170
 
@@ -194,7 +199,7 @@ If the isochrones do not overlap, `findRendezvous` returns an empty array. If no
194
199
  ## Implementing a Custom Engine
195
200
 
196
201
  ```typescript
197
- import type { RoutingEngine, LatLon, TransportMode, Isochrone, RouteMatrix } from 'rendezvous-kit'
202
+ import type { RoutingEngine, LatLon, TransportMode, Isochrone, RouteMatrix, RouteGeometry } from 'rendezvous-kit'
198
203
 
199
204
  class MyEngine implements RoutingEngine {
200
205
  readonly name = 'MyEngine'
@@ -206,6 +211,10 @@ class MyEngine implements RoutingEngine {
206
211
  async computeRouteMatrix(origins: LatLon[], destinations: LatLon[], mode: TransportMode): Promise<RouteMatrix> {
207
212
  // call your API and return a RouteMatrix
208
213
  }
214
+
215
+ async computeRoute(origin: LatLon, destination: LatLon, mode: TransportMode): Promise<RouteGeometry> {
216
+ // call your API and return a RouteGeometry with optional turn-by-turn legs
217
+ }
209
218
  }
210
219
  ```
211
220
 
@@ -235,6 +244,138 @@ Run any example with `npx tsx examples/<name>.ts`.
235
244
 
236
245
  See [llms.txt](./llms.txt) for a concise API summary, or [llms-full.txt](./llms-full.txt) for the complete reference with examples.
237
246
 
247
+ ## How the Pipeline Works
248
+
249
+ ### Step 1: Parallel isochrone computation
250
+
251
+ `findRendezvous` computes a reachability polygon for each participant in parallel.
252
+ Every participant gets their own isochrone — a polygon representing everywhere they
253
+ can reach within `maxTimeMinutes` using the specified transport mode:
254
+
255
+ ```typescript
256
+ // Internally, all N isochrones are fetched concurrently:
257
+ const isochrones = await Promise.all(
258
+ participants.map(p => engine.computeIsochrone(p, mode, maxTimeMinutes))
259
+ )
260
+ ```
261
+
262
+ This works identically for 2, 5, or 20 participants — each gets their own polygon.
263
+ The engine does the heavy lifting; rendezvous-kit just orchestrates the parallel calls.
264
+
265
+ ### Step 2: N-polygon intersection
266
+
267
+ All isochrone polygons are intersected left-to-right using Sutherland–Hodgman clipping.
268
+ The result is the geographic area reachable by **every** participant within the time budget:
269
+
270
+ ```typescript
271
+ import { intersectPolygonsAll } from 'rendezvous-kit/geo'
272
+
273
+ // Folds left-to-right: clip polygon 2 against 1, then clip 3 against that result, etc.
274
+ // Preserves disconnected components (e.g. two separate road corridors)
275
+ const components = intersectPolygonsAll(isochrones.map(iso => iso.polygon))
276
+ ```
277
+
278
+ If the intersection is empty (participants too far apart), `findRendezvous` returns `[]`.
279
+
280
+ ### Step 3: Venue search and scoring
281
+
282
+ Venues are searched within the intersection zone via Overpass API, then each venue is
283
+ scored using the chosen fairness strategy:
284
+
285
+ ```typescript
286
+ const suggestions = await findRendezvous(engine, {
287
+ participants: [alice, bob, carol],
288
+ mode: 'drive',
289
+ maxTimeMinutes: 90,
290
+ venueTypes: ['cafe', 'restaurant'],
291
+ fairness: 'min_max', // minimise the worst individual travel time
292
+ limit: 5,
293
+ })
294
+ // Each suggestion has: venue, travelTimes (per participant), fairnessScore
295
+ ```
296
+
297
+ ### When isochrones don't overlap
298
+
299
+ If the isochrones don't intersect, `findRendezvous` returns an empty array. Options:
300
+
301
+ 1. **Increase `maxTimeMinutes`** — expand the reachability polygons
302
+ 2. **Switch transport mode** — `'drive'` covers more ground than `'walk'`
303
+ 3. **Use the `hull` strategy** — for nearby participants, the library automatically uses
304
+ a convex hull of participant positions as the search region instead of isochrones.
305
+ This always produces results because it searches the area *between* participants.
306
+
307
+ If the intersection exists but contains no matching venues, the library falls back to
308
+ a synthetic "Meeting point" at the area-weighted centroid of the intersection.
309
+
310
+ ## Switching Routing Engines
311
+
312
+ OSRM does not support isochrone computation. If you're using OSRM and need the full
313
+ pipeline, swap to an isochrone-capable engine:
314
+
315
+ ```typescript
316
+ // Before: OSRM (route matrix only — no isochrones)
317
+ import { OsrmEngine } from 'rendezvous-kit/engines/osrm'
318
+ const engine = new OsrmEngine({ baseUrl: 'http://localhost:5000' })
319
+
320
+ // After: Valhalla (self-hosted, free, full isochrone support)
321
+ import { ValhallaEngine } from 'rendezvous-kit/engines/valhalla'
322
+ const engine = new ValhallaEngine({ baseUrl: 'http://localhost:8002' })
323
+
324
+ // Or: OpenRouteService (hosted API, requires free API key)
325
+ import { OpenRouteServiceEngine } from 'rendezvous-kit/engines/openrouteservice'
326
+ const engine = new OpenRouteServiceEngine({
327
+ apiKey: process.env.ORS_API_KEY!,
328
+ baseUrl: 'https://api.openrouteservice.org', // default
329
+ })
330
+
331
+ // Or: GraphHopper (self-hosted or hosted, API key optional for self-hosted)
332
+ import { GraphHopperEngine } from 'rendezvous-kit/engines/graphhopper'
333
+ const engine = new GraphHopperEngine({
334
+ baseUrl: 'http://localhost:8989',
335
+ apiKey: process.env.GRAPHHOPPER_KEY, // optional for self-hosted
336
+ })
337
+
338
+ // All engines implement the same RoutingEngine interface.
339
+ // The rest of your code stays identical:
340
+ const suggestions = await findRendezvous(engine, { participants, mode, ... })
341
+ ```
342
+
343
+ For self-hosting guides (Docker one-liners for Valhalla, OSRM, GraphHopper), see
344
+ [Self-Hosting a Routing Engine](./docs/self-hosting-a-routing-engine.md).
345
+
346
+ ## Architecture
347
+
348
+ rendezvous-kit's pipeline combines two kinds of spatial operations:
349
+
350
+ **Routing engine operations** (external): isochrone computation, route matrix, and
351
+ route geometry. These call your chosen engine's HTTP API for real road-network
352
+ calculations.
353
+
354
+ **Local geometry operations** (pure TypeScript, no dependencies): polygon intersection
355
+ (Sutherland–Hodgman), area calculation (shoelace), centroid computation, bounding boxes,
356
+ and convex hull (via `geohash-kit/coverage`). These run entirely in-process with no
357
+ network calls.
358
+
359
+ The only external dependency is `geohash-kit`, which provides the `convexHull` function
360
+ used by the hull-strategy fast path for nearby participants. All other polygon operations
361
+ (intersection, clipping, triangulation, area) are implemented in `rendezvous-kit/geo`.
362
+
363
+ ```
364
+ Participants ──→ Engine.computeIsochrone() ──→ Polygon[]
365
+
366
+ intersectPolygonsAll() ← local geometry
367
+
368
+ Intersection zone
369
+
370
+ searchVenues() via Overpass API
371
+
372
+ Engine.computeRouteMatrix() ──→ travel times
373
+
374
+ scoreVenues() ← local fairness calc
375
+
376
+ Ranked suggestions
377
+ ```
378
+
238
379
  ## Troubleshooting
239
380
 
240
381
  **`findRendezvous` returns an empty array**
@@ -252,6 +393,23 @@ Check that your engine base URL is correct and the service is running. For ORS,
252
393
  **OSRM: `Error: OSRM does not support isochrone computation`**
253
394
  OSRM cannot generate isochrones. Use Valhalla, OpenRouteService, or GraphHopper instead. OSRM is supported only for route matrix computation.
254
395
 
396
+ ## Part of the ForgeSworn Toolkit
397
+
398
+ [ForgeSworn](https://forgesworn.dev) builds open-source cryptographic identity, payments, and coordination tools for Nostr.
399
+
400
+ | Library | What it does |
401
+ |---------|-------------|
402
+ | [nsec-tree](https://github.com/forgesworn/nsec-tree) | Deterministic sub-identity derivation |
403
+ | [ring-sig](https://github.com/forgesworn/ring-sig) | SAG/LSAG ring signatures on secp256k1 |
404
+ | [range-proof](https://github.com/forgesworn/range-proof) | Pedersen commitment range proofs |
405
+ | [canary-kit](https://github.com/forgesworn/canary-kit) | Coercion-resistant spoken verification |
406
+ | [spoken-token](https://github.com/forgesworn/spoken-token) | Human-speakable verification tokens |
407
+ | [toll-booth](https://github.com/forgesworn/toll-booth) | L402 payment middleware |
408
+ | [geohash-kit](https://github.com/forgesworn/geohash-kit) | Geohash toolkit with polygon coverage |
409
+ | [nostr-attestations](https://github.com/forgesworn/nostr-attestations) | NIP-VA verifiable attestations |
410
+ | [dominion](https://github.com/forgesworn/dominion) | Epoch-based encrypted access control |
411
+ | [nostr-veil](https://github.com/forgesworn/nostr-veil) | Privacy-preserving Web of Trust |
412
+
255
413
  ## Licence
256
414
 
257
415
  [MIT](https://github.com/forgesworn/rendezvous-kit/blob/main/LICENSE)
package/llms-full.txt CHANGED
@@ -63,6 +63,15 @@ type VenueType =
63
63
  | 'pub'
64
64
  | 'playground'
65
65
  | 'community_centre'
66
+ | 'bar'
67
+ | 'fast_food'
68
+ | 'garden'
69
+ | 'theatre'
70
+ | 'arts_centre'
71
+ | 'fitness_centre'
72
+ | 'sports_centre'
73
+ | 'escape_game'
74
+ | 'swimming_pool'
66
75
  | string
67
76
 
68
77
  /** Result of an isochrone computation. */
@@ -97,6 +106,43 @@ interface Venue {
97
106
  osmId?: string // e.g. 'node/123456'
98
107
  }
99
108
 
109
+ /** GeoJSON LineString geometry. */
110
+ interface GeoJSONLineString {
111
+ type: 'LineString'
112
+ coordinates: number[][]
113
+ }
114
+
115
+ /** A single manoeuvre in a route. */
116
+ interface RouteLeg {
117
+ instruction: string
118
+ distanceKm: number
119
+ durationMinutes: number
120
+ type?: number // Valhalla manoeuvre type (0-38)
121
+ streetNames?: string[]
122
+ beginStreetNames?: string[]
123
+ verbalInstruction?: string
124
+ toll?: boolean
125
+ highway?: boolean
126
+ ferry?: boolean
127
+ rough?: boolean
128
+ gate?: boolean
129
+ bearingBefore?: number
130
+ bearingAfter?: number
131
+ beginShapeIndex?: number
132
+ endShapeIndex?: number
133
+ }
134
+
135
+ /** Result of a single route computation. */
136
+ interface RouteGeometry {
137
+ origin: LatLon
138
+ destination: LatLon
139
+ mode: TransportMode
140
+ durationMinutes: number
141
+ distanceKm: number
142
+ geometry: GeoJSONLineString
143
+ legs?: RouteLeg[]
144
+ }
145
+
100
146
  /** Options for rendezvous calculation. */
101
147
  interface RendezvousOptions {
102
148
  participants: LatLon[] // at least 2 required
@@ -105,6 +151,7 @@ interface RendezvousOptions {
105
151
  venueTypes: VenueType[]
106
152
  fairness?: FairnessStrategy // default: 'min_max'
107
153
  limit?: number // max suggestions to return, default: 5
154
+ strategy?: 'auto' | 'hull' | 'isochrone' // pipeline strategy, default: 'auto'
108
155
  }
109
156
 
110
157
  /** A ranked rendezvous suggestion. */
@@ -112,6 +159,7 @@ interface RendezvousSuggestion {
112
159
  venue: Venue
113
160
  travelTimes: Record<string, number> // keyed by participant label or 'participant_N'
114
161
  fairnessScore: number // lower is better
162
+ metadata?: { strategy: 'hull' | 'isochrone' } // which pipeline was used
115
163
  }
116
164
 
117
165
  /** Engine-agnostic routing interface. Implement this to add a custom engine. */
@@ -119,6 +167,7 @@ interface RoutingEngine {
119
167
  readonly name: string
120
168
  computeIsochrone(origin: LatLon, mode: TransportMode, timeMinutes: number): Promise<Isochrone>
121
169
  computeRouteMatrix(origins: LatLon[], destinations: LatLon[], mode: TransportMode): Promise<RouteMatrix>
170
+ computeRoute(origin: LatLon, destination: LatLon, mode: TransportMode): Promise<RouteGeometry>
122
171
  }
123
172
 
124
173
  /** Bounding box (from rendezvous-kit/geo). */
@@ -413,6 +462,15 @@ Built-in `VenueType` to Overpass tag mapping:
413
462
  | `pub` | `amenity=pub` |
414
463
  | `playground` | `leisure=playground` |
415
464
  | `community_centre` | `amenity=community_centre` |
465
+ | `bar` | `amenity=bar` |
466
+ | `fast_food` | `amenity=fast_food` |
467
+ | `garden` | `leisure=garden` |
468
+ | `theatre` | `amenity=theatre` |
469
+ | `arts_centre` | `amenity=arts_centre` |
470
+ | `fitness_centre` | `leisure=fitness_centre` |
471
+ | `sports_centre` | `leisure=sports_centre` |
472
+ | `escape_game` | `leisure=escape_game` |
473
+ | `swimming_pool` | `leisure=swimming_pool` |
416
474
 
417
475
  Unknown strings are passed through as `amenity=<value>`.
418
476
 
@@ -421,7 +479,7 @@ Unknown strings are passed through as `amenity=<value>`.
421
479
  ## Implementing a Custom Engine
422
480
 
423
481
  ```typescript
424
- import type { RoutingEngine, LatLon, TransportMode, Isochrone, RouteMatrix } from 'rendezvous-kit'
482
+ import type { RoutingEngine, LatLon, TransportMode, Isochrone, RouteMatrix, RouteGeometry } from 'rendezvous-kit'
425
483
 
426
484
  class MyRoutingEngine implements RoutingEngine {
427
485
  readonly name = 'MyEngine'
@@ -454,6 +512,27 @@ class MyRoutingEngine implements RoutingEngine {
454
512
  }
455
513
  return { origins, destinations, entries }
456
514
  }
515
+
516
+ async computeRoute(
517
+ origin: LatLon,
518
+ destination: LatLon,
519
+ mode: TransportMode,
520
+ ): Promise<RouteGeometry> {
521
+ const raw = await myApi.getRoute(origin, destination, mode)
522
+ return {
523
+ origin,
524
+ destination,
525
+ mode,
526
+ durationMinutes: raw.seconds / 60,
527
+ distanceKm: raw.metres / 1000,
528
+ geometry: { type: 'LineString', coordinates: raw.polyline },
529
+ legs: raw.steps.map((s: any) => ({
530
+ instruction: s.text,
531
+ distanceKm: s.metres / 1000,
532
+ durationMinutes: s.seconds / 60,
533
+ })),
534
+ }
535
+ }
457
536
  }
458
537
 
459
538
  // Use it with findRendezvous
@@ -464,12 +543,12 @@ const suggestions = await findRendezvous(new MyRoutingEngine(), options)
464
543
 
465
544
  ## Engine Comparison
466
545
 
467
- | Engine | Isochrone | Matrix | Auth | Notes |
468
- |--------|:---------:|:------:|------|-------|
469
- | Valhalla | Yes | Yes | None | Best self-hosted option; supports public transit |
470
- | OpenRouteService | Yes | Yes | API key | Free tier available; no public transit |
471
- | GraphHopper | Yes | Yes | Optional | API key only for cloud; optional self-hosted |
472
- | OSRM | No | Yes | None | Fastest matrix; no isochrone support |
546
+ | Engine | Isochrone | Matrix | Route | Auth | Notes |
547
+ |--------|:---------:|:------:|:-----:|------|-------|
548
+ | Valhalla | Yes | Yes | Yes | None | Best self-hosted option; supports public transit |
549
+ | OpenRouteService | Yes | Yes | No | API key | Free tier available; no public transit |
550
+ | GraphHopper | Yes | Yes | No | Optional | API key only for cloud; optional self-hosted |
551
+ | OSRM | No | Yes | No | None | Fastest matrix; no isochrone support |
473
552
 
474
553
  ---
475
554
 
package/llms.txt CHANGED
@@ -52,10 +52,13 @@ Pure-TypeScript polygon geometry.
52
52
  - GeoJSONPolygon: { type: 'Polygon', coordinates: number[][][] }
53
53
  - TransportMode: 'drive' | 'cycle' | 'walk' | 'public_transit'
54
54
  - FairnessStrategy: 'min_max' | 'min_total' | 'min_variance'
55
- - VenueType: 'park' | 'cafe' | 'restaurant' | 'service_station' | 'library' | 'pub' | 'playground' | 'community_centre' | string
56
- - RendezvousOptions: { participants, mode, maxTimeMinutes, venueTypes, fairness?, limit? }
57
- - RendezvousSuggestion: { venue, travelTimes: Record<string, number>, fairnessScore }
58
- - RoutingEngine: interface { name, computeIsochrone, computeRouteMatrix }
55
+ - VenueType: 'park' | 'cafe' | 'restaurant' | 'service_station' | 'library' | 'pub' | 'playground' | 'community_centre' | 'bar' | 'fast_food' | 'garden' | 'theatre' | 'arts_centre' | 'fitness_centre' | 'sports_centre' | 'escape_game' | 'swimming_pool' | string
56
+ - RendezvousOptions: { participants, mode, maxTimeMinutes, venueTypes, fairness?, limit?, strategy? }
57
+ - RendezvousSuggestion: { venue, travelTimes: Record<string, number>, fairnessScore, metadata? }
58
+ - RoutingEngine: interface { name, computeIsochrone, computeRouteMatrix, computeRoute }
59
+ - RouteGeometry: { origin, destination, mode, durationMinutes, distanceKm, geometry: GeoJSONLineString, legs?: RouteLeg[] }
60
+ - RouteLeg: { instruction, distanceKm, durationMinutes, type?, streetNames?, verbalInstruction?, ... }
61
+ - GeoJSONLineString: { type: 'LineString', coordinates: number[][] }
59
62
 
60
63
  ## Fairness Strategies
61
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rendezvous-kit",
3
- "version": "1.21.4",
3
+ "version": "1.21.6",
4
4
  "type": "module",
5
5
  "description": "Find fair meeting points for N people — isochrone intersection, venue search, and fairness scoring.",
6
6
  "main": "./dist/index.js",
@@ -85,6 +85,6 @@
85
85
  "homepage": "https://github.com/forgesworn/rendezvous-kit",
86
86
  "funding": {
87
87
  "type": "lightning",
88
- "url": "https://strike.me/thedonkey"
88
+ "url": "lightning:thedonkey@strike.me"
89
89
  }
90
90
  }