Skip to content

Commit ab685dc

Browse files
docs: add examples, guides, and expanded keywords for AI discoverability
1 parent 9bc29c7 commit ab685dc

7 files changed

Lines changed: 383 additions & 0 deletions

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,21 @@ class MyEngine implements RoutingEngine {
209209
}
210210
```
211211

212+
## Examples
213+
214+
Runnable examples in [`examples/`](./examples/):
215+
216+
- **[basic-usage.ts](./examples/basic-usage.ts)** — find a fair meeting point for three people
217+
- **[comparing-fairness-strategies.ts](./examples/comparing-fairness-strategies.ts)** — see how min_max, min_total, and min_variance rank differently
218+
- **[custom-engine.ts](./examples/custom-engine.ts)** — implement the RoutingEngine interface with a mock engine
219+
220+
Run any example with `npx tsx examples/<name>.ts`.
221+
222+
## Guides
223+
224+
- **[Choosing a Fairness Strategy](./docs/choosing-a-fairness-strategy.md)** — when to use min_max vs min_total vs min_variance
225+
- **[Self-Hosting a Routing Engine](./docs/self-hosting-a-routing-engine.md)** — run Valhalla, OSRM, or GraphHopper locally with Docker
226+
212227
## Companion Library
213228

214229
**geohash-kit** — spatial primitives (pointInPolygon, GeoJSON types, distance utilities) used internally by rendezvous-kit.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Choosing a Fairness Strategy
2+
3+
rendezvous-kit scores candidate venues using a **fairness strategy** — a function that reduces each participant's travel time into a single score. Lower scores are better.
4+
5+
## The Three Strategies
6+
7+
### `min_max` — Nobody travels excessively (default)
8+
9+
Optimises for the **worst-case** travel time. The score is the longest individual journey.
10+
11+
**Best for:** Social meetups, events where one person travelling much further than everyone else feels unfair.
12+
13+
```
14+
Alice: 30 min, Bob: 45 min, Carol: 20 min → score = 45
15+
```
16+
17+
### `min_total` — Minimise group effort
18+
19+
Optimises for the **sum** of all travel times. The score is the total minutes spent travelling across the whole group.
20+
21+
**Best for:** Logistics, delivery hubs, or scenarios where total cost (fuel, time, emissions) matters more than individual fairness.
22+
23+
```
24+
Alice: 30 min, Bob: 45 min, Carol: 20 min → score = 95
25+
```
26+
27+
### `min_variance` — Everyone travels roughly equally
28+
29+
Optimises for **equalising** travel times. The score is the standard deviation of individual journey times.
30+
31+
**Best for:** Repeated meetings (e.g. weekly team syncs) where perceived fairness over time matters. Everyone should feel they are making a similar effort.
32+
33+
```
34+
Alice: 30 min, Bob: 45 min, Carol: 20 min → score = 10.3
35+
Alice: 35 min, Bob: 32 min, Carol: 33 min → score = 1.2 ← preferred
36+
```
37+
38+
## When They Disagree
39+
40+
The three strategies often pick different top venues. Consider a scenario with two candidate cafés:
41+
42+
| Café | Alice | Bob | Carol | min_max | min_total | min_variance |
43+
|------|-------|-----|-------|---------|-----------|--------------|
44+
| The Fox | 25 min | 50 min | 25 min | 50 | 100 | 11.8 |
45+
| The Bear | 35 min | 35 min | 35 min | 35 | 105 | 0.0 |
46+
47+
- **min_max** picks The Bear (worst case is 35 vs 50)
48+
- **min_total** picks The Fox (total is 100 vs 105)
49+
- **min_variance** picks The Bear (perfectly equal)
50+
51+
There is no universally "correct" strategy — choose based on what matters to your users.
52+
53+
## Usage
54+
55+
```typescript
56+
const suggestions = await findRendezvous(engine, {
57+
participants: [alice, bob, carol],
58+
mode: 'drive',
59+
maxTimeMinutes: 60,
60+
venueTypes: ['cafe'],
61+
fairness: 'min_variance', // or 'min_max' (default), 'min_total'
62+
})
63+
```
64+
65+
## Comparing Strategies
66+
67+
See [`examples/comparing-fairness-strategies.ts`](../examples/comparing-fairness-strategies.ts) for a runnable script that shows how the three strategies rank the same venues differently.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Self-Hosting a Routing Engine
2+
3+
rendezvous-kit is engine-agnostic — it works with any routing service that can compute isochrones and route matrices. This guide covers self-hosting options so you can run the full pipeline without third-party API keys or rate limits.
4+
5+
## Quick Comparison
6+
7+
| Engine | Isochrone | Route Matrix | Docker image | Typical RAM | Setup effort |
8+
|--------|:---------:|:------------:|--------------|-------------|--------------|
9+
| Valhalla | Yes | Yes | `ghcr.io/gis-ops/docker-valhalla` | 2–8 GB | Medium |
10+
| OSRM | No | Yes | `osrm/osrm-backend` | 1–4 GB | Low |
11+
| GraphHopper | Yes | Yes | `graphhopper/graphhopper` | 2–6 GB | Low |
12+
13+
**Recommendation:** Valhalla is the best all-round choice — it supports isochrones, route matrices, and turn-by-turn routing. OSRM is lighter but cannot compute isochrones, so you would need to supply your own intersection polygon or pair it with another engine.
14+
15+
## Valhalla (Recommended)
16+
17+
The easiest way to run Valhalla is with the [GIS-OPS Docker image](https://github.com/gis-ops/docker-valhalla):
18+
19+
```bash
20+
docker run -d --name valhalla \
21+
-p 8002:8002 \
22+
-e tile_urls=https://download.geofabrik.de/europe/great-britain-latest.osm.pbf \
23+
-v valhalla_data:/custom_files \
24+
ghcr.io/gis-ops/docker-valhalla/valhalla:latest
25+
```
26+
27+
The first run downloads and builds routing tiles — this can take 10–60 minutes depending on the region size. Subsequent starts are fast.
28+
29+
**Use in rendezvous-kit:**
30+
31+
```typescript
32+
import { ValhallaEngine } from 'rendezvous-kit/engines/valhalla'
33+
34+
const engine = new ValhallaEngine({ baseUrl: 'http://localhost:8002' })
35+
```
36+
37+
### Choosing a Region
38+
39+
Download `.osm.pbf` extracts from [Geofabrik](https://download.geofabrik.de/). Smaller regions use less RAM and build faster:
40+
41+
- `great-britain-latest.osm.pbf`~1.2 GB, builds in ~15 min, ~4 GB RAM
42+
- `europe-latest.osm.pbf`~28 GB, builds in hours, ~16 GB RAM
43+
- Country-level extracts are a good compromise
44+
45+
### Multiple Regions
46+
47+
To cover multiple non-contiguous regions, download separate extracts and merge them with [Osmium](https://osmcode.org/osmium-tool/):
48+
49+
```bash
50+
osmium merge england.osm.pbf scotland.osm.pbf wales.osm.pbf -o gb.osm.pbf
51+
```
52+
53+
## OSRM
54+
55+
OSRM is fast and lightweight but only supports route matrices — no isochrones.
56+
57+
```bash
58+
# Download and prepare data
59+
wget https://download.geofabrik.de/europe/great-britain-latest.osm.pbf
60+
docker run -t -v $(pwd):/data osrm/osrm-backend osrm-extract -p /opt/car.lua /data/great-britain-latest.osm.pbf
61+
docker run -t -v $(pwd):/data osrm/osrm-backend osrm-partition /data/great-britain-latest.osrm
62+
docker run -t -v $(pwd):/data osrm/osrm-backend osrm-customize /data/great-britain-latest.osrm
63+
64+
# Run the server
65+
docker run -d --name osrm \
66+
-p 5000:5000 \
67+
-v $(pwd):/data \
68+
osrm/osrm-backend osrm-routed --algorithm mld /data/great-britain-latest.osrm
69+
```
70+
71+
**Use in rendezvous-kit:**
72+
73+
```typescript
74+
import { OsrmEngine } from 'rendezvous-kit/engines/osrm'
75+
76+
const engine = new OsrmEngine({ baseUrl: 'http://localhost:5000' })
77+
// Note: OSRM cannot compute isochrones — findRendezvous will throw.
78+
// Use OSRM for computeRouteMatrix only.
79+
```
80+
81+
## GraphHopper
82+
83+
GraphHopper supports isochrones and route matrices. The open-source version is free to self-host.
84+
85+
```bash
86+
docker run -d --name graphhopper \
87+
-p 8989:8989 \
88+
-e JAVA_OPTS="-Xmx4g" \
89+
-v graphhopper_data:/data \
90+
graphhopper/graphhopper:latest \
91+
--url https://download.geofabrik.de/europe/great-britain-latest.osm.pbf \
92+
--host 0.0.0.0
93+
```
94+
95+
**Use in rendezvous-kit:**
96+
97+
```typescript
98+
import { GraphHopperEngine } from 'rendezvous-kit/engines/graphhopper'
99+
100+
const engine = new GraphHopperEngine({ baseUrl: 'http://localhost:8989' })
101+
```
102+
103+
## Cloud-Hosted Alternatives
104+
105+
If you prefer not to self-host:
106+
107+
- **OpenRouteService** — free API key from [openrouteservice.org](https://openrouteservice.org/dev/), rate-limited (40 requests/min on free tier)
108+
- **Trotters Routing**`https://routing.trotters.cc`, L402-gated (pay-per-request with Lightning)
109+
110+
```typescript
111+
import { OpenRouteServiceEngine } from 'rendezvous-kit/engines/openrouteservice'
112+
113+
const engine = new OpenRouteServiceEngine({ apiKey: 'your-api-key' })
114+
```
115+
116+
## Tips
117+
118+
- **Start with a small region** while developing — city or country level. You can always upgrade later.
119+
- **Pin the PBF date** in production so tile rebuilds are reproducible.
120+
- **Monitor memory** — routing engines are memory-hungry. Valhalla with GB data uses ~4 GB RAM.
121+
- **Health checks** — all engines respond to `GET /` or similar. Add a health check to your Docker Compose or Kubernetes config.

examples/basic-usage.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Basic usage — find a fair meeting point for three people.
3+
*
4+
* Prerequisites:
5+
* npm install rendezvous-kit
6+
* A running Valhalla instance (or swap for OpenRouteServiceEngine with an API key)
7+
*
8+
* Run:
9+
* npx tsx examples/basic-usage.ts
10+
*/
11+
import { findRendezvous } from 'rendezvous-kit'
12+
import { ValhallaEngine } from 'rendezvous-kit/engines/valhalla'
13+
14+
const engine = new ValhallaEngine({ baseUrl: 'http://localhost:8002' })
15+
16+
const suggestions = await findRendezvous(engine, {
17+
participants: [
18+
{ lat: 51.5074, lon: -0.1278, label: 'Alice' }, // London
19+
{ lat: 51.4545, lon: -2.5879, label: 'Bob' }, // Bristol
20+
{ lat: 52.4862, lon: -1.8904, label: 'Carol' }, // Birmingham
21+
],
22+
mode: 'drive',
23+
maxTimeMinutes: 90,
24+
venueTypes: ['cafe', 'restaurant'],
25+
fairness: 'min_max', // minimise the worst-case travel time
26+
limit: 5,
27+
})
28+
29+
if (suggestions.length === 0) {
30+
console.log('No overlap — participants are too far apart for the given time budget.')
31+
} else {
32+
for (const s of suggestions) {
33+
console.log(`${s.venue.name} (${s.venue.venueType})`)
34+
console.log(` Fairness score: ${s.fairnessScore.toFixed(1)} min`)
35+
console.log(' Travel times:', s.travelTimes)
36+
console.log()
37+
}
38+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Compare fairness strategies — see how min_max, min_total, and min_variance
3+
* produce different rankings for the same participants and venues.
4+
*
5+
* Prerequisites:
6+
* npm install rendezvous-kit
7+
* A running Valhalla instance (or swap engine)
8+
*
9+
* Run:
10+
* npx tsx examples/comparing-fairness-strategies.ts
11+
*/
12+
import { findRendezvous } from 'rendezvous-kit'
13+
import { ValhallaEngine } from 'rendezvous-kit/engines/valhalla'
14+
import type { FairnessStrategy } from 'rendezvous-kit'
15+
16+
const engine = new ValhallaEngine({ baseUrl: 'http://localhost:8002' })
17+
18+
const participants = [
19+
{ lat: 51.5074, lon: -0.1278, label: 'Alice' }, // London
20+
{ lat: 51.4545, lon: -2.5879, label: 'Bob' }, // Bristol
21+
{ lat: 52.4862, lon: -1.8904, label: 'Carol' }, // Birmingham
22+
]
23+
24+
const strategies: FairnessStrategy[] = ['min_max', 'min_total', 'min_variance']
25+
26+
for (const fairness of strategies) {
27+
console.log(`\n=== Strategy: ${fairness} ===\n`)
28+
29+
const suggestions = await findRendezvous(engine, {
30+
participants,
31+
mode: 'drive',
32+
maxTimeMinutes: 90,
33+
venueTypes: ['cafe', 'pub'],
34+
fairness,
35+
limit: 3,
36+
})
37+
38+
if (suggestions.length === 0) {
39+
console.log('No results — try increasing maxTimeMinutes.')
40+
continue
41+
}
42+
43+
for (const s of suggestions) {
44+
const times = Object.entries(s.travelTimes)
45+
.map(([who, mins]) => `${who}: ${mins} min`)
46+
.join(', ')
47+
console.log(` ${s.venue.name} — score: ${s.fairnessScore.toFixed(1)}, ${times}`)
48+
}
49+
}

examples/custom-engine.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Implement a custom routing engine.
3+
*
4+
* This example shows how to implement the RoutingEngine interface with a mock
5+
* engine, which is useful for testing or when wrapping a proprietary API.
6+
*
7+
* Run:
8+
* npx tsx examples/custom-engine.ts
9+
*/
10+
import { findRendezvous } from 'rendezvous-kit'
11+
import type { RoutingEngine, LatLon, TransportMode, Isochrone, RouteMatrix, RouteGeometry, MatrixEntry } from 'rendezvous-kit'
12+
import { circleToPolygon } from 'rendezvous-kit/geo'
13+
14+
/**
15+
* A mock engine that generates circular isochrones and straight-line travel times.
16+
* Replace the method bodies with calls to your actual routing API.
17+
*/
18+
class MockEngine implements RoutingEngine {
19+
readonly name = 'MockEngine'
20+
21+
async computeIsochrone(origin: LatLon, mode: TransportMode, timeMinutes: number): Promise<Isochrone> {
22+
// Approximate reachable area as a circle. Real engines return road-network-shaped polygons.
23+
const speedKmh = mode === 'drive' ? 80 : mode === 'cycle' ? 20 : 5
24+
const radiusMetres = (speedKmh * timeMinutes / 60) * 1000
25+
26+
return {
27+
origin,
28+
mode,
29+
timeMinutes,
30+
polygon: circleToPolygon([origin.lon, origin.lat], radiusMetres),
31+
}
32+
}
33+
34+
async computeRouteMatrix(origins: LatLon[], destinations: LatLon[], mode: TransportMode): Promise<RouteMatrix> {
35+
const speedKmh = mode === 'drive' ? 80 : mode === 'cycle' ? 20 : 5
36+
const entries: MatrixEntry[] = []
37+
38+
for (let oi = 0; oi < origins.length; oi++) {
39+
for (let di = 0; di < destinations.length; di++) {
40+
const distKm = haversineKm(origins[oi], destinations[di])
41+
entries.push({
42+
originIndex: oi,
43+
destinationIndex: di,
44+
durationMinutes: (distKm / speedKmh) * 60,
45+
distanceKm: distKm,
46+
})
47+
}
48+
}
49+
50+
return { origins, destinations, entries }
51+
}
52+
53+
async computeRoute(_origin: LatLon, _destination: LatLon, _mode: TransportMode): Promise<RouteGeometry> {
54+
throw new Error('computeRoute not implemented in MockEngine')
55+
}
56+
}
57+
58+
function haversineKm(a: LatLon, b: LatLon): number {
59+
const R = 6371
60+
const dLat = (b.lat - a.lat) * Math.PI / 180
61+
const dLon = (b.lon - a.lon) * Math.PI / 180
62+
const sinLat = Math.sin(dLat / 2)
63+
const sinLon = Math.sin(dLon / 2)
64+
const h = sinLat * sinLat + Math.cos(a.lat * Math.PI / 180) * Math.cos(b.lat * Math.PI / 180) * sinLon * sinLon
65+
return R * 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h))
66+
}
67+
68+
// --- Run it ---
69+
70+
const engine = new MockEngine()
71+
72+
const suggestions = await findRendezvous(engine, {
73+
participants: [
74+
{ lat: 51.5074, lon: -0.1278, label: 'Alice' },
75+
{ lat: 51.4545, lon: -2.5879, label: 'Bob' },
76+
],
77+
mode: 'drive',
78+
maxTimeMinutes: 60,
79+
venueTypes: ['cafe'],
80+
limit: 3,
81+
})
82+
83+
console.log(`Found ${suggestions.length} suggestion(s) using ${engine.name}:`)
84+
for (const s of suggestions) {
85+
console.log(` ${s.venue.name} — score: ${s.fairnessScore.toFixed(1)} min`)
86+
console.log(' Travel times:', s.travelTimes)
87+
}

0 commit comments

Comments
 (0)