@proximap/cli 1.0.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/LICENSE +21 -0
- package/README.md +89 -0
- package/dist/index.js +568 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ameya Borkar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @proximap/cli
|
|
2
|
+
|
|
3
|
+
The `proximap` command-line tool — find and rank what's near any place, powered
|
|
4
|
+
by OpenStreetMap. No API keys required.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g @proximap/cli
|
|
10
|
+
# or run without installing:
|
|
11
|
+
npx @proximap/cli near "Eiffel Tower, Paris"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
| Command | What it does |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `near <place>` | Nearby amenities, ranked by distance (or `--by travel-time`). Filters, accessibility, open-now, export, offline. |
|
|
19
|
+
| `geocode <place>` | Resolve a name to coordinates; warns and lists candidates when ambiguous. |
|
|
20
|
+
| `gaps <place>` | Which everyday amenities are missing nearby (framed as "not found in OSM"). |
|
|
21
|
+
| `score <place>` | Walkability score 0–100 with a per-category breakdown and confidence. |
|
|
22
|
+
| `compare <a> <b> …` | Rank 2+ locations by weighted access to what you care about. |
|
|
23
|
+
| `reachable <place>` | What's reachable within a time budget (isochrone). |
|
|
24
|
+
| `errands <place>` | Shortest trip that hits one of each category (Generalized TSP). |
|
|
25
|
+
| `snapshot <place>` | Capture an area's POIs to a file for offline reuse. |
|
|
26
|
+
| `bulk <file>` | Walkability-score many locations from a file → CSV. |
|
|
27
|
+
|
|
28
|
+
Run `proximap --help` or `proximap <command> --help` for all flags.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Nearby amenities within 800 m, ranked by distance
|
|
34
|
+
proximap near "Eiffel Tower, Paris" --radius 800 --limit 20
|
|
35
|
+
|
|
36
|
+
# Filter by category, output JSON
|
|
37
|
+
proximap near "MG Road, Bengaluru" --category healthcare --category food --json
|
|
38
|
+
|
|
39
|
+
# Compose facets: a vegan place that does takeaway and accepts contactless
|
|
40
|
+
proximap near "Kreuzberg, Berlin" -c food --filter diet=vegan --filter takeaway --filter payment=contactless
|
|
41
|
+
|
|
42
|
+
# Accessibility-first: rank step-free / wheelchair-accessible places first
|
|
43
|
+
proximap near "Bahnhofstrasse, Zürich" -c food --accessible
|
|
44
|
+
|
|
45
|
+
# Only what's open right now (or at a future time); unknown hours are kept + labelled
|
|
46
|
+
proximap near "Shibuya, Tokyo" -c coffee --open-now
|
|
47
|
+
proximap near "Shibuya, Tokyo" -c food --open-at 2026-06-20T21:00
|
|
48
|
+
|
|
49
|
+
# Rank by real walking/cycling/driving time (key-free Valhalla routing)
|
|
50
|
+
proximap near "Alexanderplatz, Berlin" -c cafe --by travel-time --mode walk
|
|
51
|
+
|
|
52
|
+
# What's reachable within a 15-minute walk? (isochrone)
|
|
53
|
+
proximap reachable "Alexanderplatz, Berlin" --within 15min --mode walk -c grocery
|
|
54
|
+
|
|
55
|
+
# Shortest trip that hits one of each: pharmacy AND atm AND grocery (Generalized TSP)
|
|
56
|
+
proximap errands "Alexanderplatz, Berlin" -c pharmacy -c atm -c grocery
|
|
57
|
+
|
|
58
|
+
# Works with coordinates too
|
|
59
|
+
proximap near "48.8584,2.2945"
|
|
60
|
+
|
|
61
|
+
# Resolve a place to coordinates
|
|
62
|
+
proximap geocode "Sydney Opera House"
|
|
63
|
+
|
|
64
|
+
# What everyday amenities are MISSING nearby? (framed as "not found in OSM")
|
|
65
|
+
proximap gaps "Brandenburg Gate, Berlin" --radius 3000 --threshold 1000
|
|
66
|
+
|
|
67
|
+
# How walkable / well-served is this address? (0-100, with a breakdown)
|
|
68
|
+
proximap score "Brandenburg Gate, Berlin"
|
|
69
|
+
|
|
70
|
+
# Compare places to live by access to what you care about
|
|
71
|
+
proximap compare "Prenzlauer Berg, Berlin" "Marzahn, Berlin" --weights grocery=3,transport=2,park=2
|
|
72
|
+
|
|
73
|
+
# Export results for GIS / spreadsheets (OSM data is yours to store under ODbL)
|
|
74
|
+
proximap near "Eiffel Tower, Paris" -c food --format geojson > food.geojson
|
|
75
|
+
proximap near "Eiffel Tower, Paris" -c food --format csv > food.csv
|
|
76
|
+
|
|
77
|
+
# Snapshot an area once, then query it offline with no network calls
|
|
78
|
+
proximap snapshot "Montmartre, Paris" --radius 1500 --out montmartre.json
|
|
79
|
+
proximap near "48.8867,2.3431" -c cafe --dataset montmartre.json
|
|
80
|
+
|
|
81
|
+
# Bulk-score many locations from a file → CSV
|
|
82
|
+
proximap bulk addresses.txt > scores.csv
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Run `proximap --help` or `proximap <command> --help` for all options.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import {
|
|
6
|
+
compareLocations,
|
|
7
|
+
DatasetPlacesProvider,
|
|
8
|
+
detectGaps,
|
|
9
|
+
disambiguateLocation,
|
|
10
|
+
findNearbyAmenities,
|
|
11
|
+
ODBL_ATTRIBUTION,
|
|
12
|
+
planErrands,
|
|
13
|
+
reachableAmenities,
|
|
14
|
+
snapshotArea,
|
|
15
|
+
toCSV,
|
|
16
|
+
toGeoJSON,
|
|
17
|
+
ValhallaRoutingProvider,
|
|
18
|
+
walkabilityScore
|
|
19
|
+
} from "@proximap/core";
|
|
20
|
+
import { Command } from "commander";
|
|
21
|
+
|
|
22
|
+
// src/render.ts
|
|
23
|
+
import {
|
|
24
|
+
CATEGORY_LABELS,
|
|
25
|
+
formatDistance,
|
|
26
|
+
formatDuration
|
|
27
|
+
} from "@proximap/core";
|
|
28
|
+
import pc from "picocolors";
|
|
29
|
+
var MODE_WORDS = { walk: "walking", bike: "cycling", drive: "driving" };
|
|
30
|
+
function coordString(lat, lng) {
|
|
31
|
+
return `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
|
|
32
|
+
}
|
|
33
|
+
function renderNearby(result) {
|
|
34
|
+
const { origin, results, total, routing } = result;
|
|
35
|
+
const lines = [
|
|
36
|
+
pc.bold(origin.displayName),
|
|
37
|
+
pc.dim(
|
|
38
|
+
`${coordString(origin.location.lat, origin.location.lng)} \xB7 ${total} found, showing ${results.length}`
|
|
39
|
+
)
|
|
40
|
+
];
|
|
41
|
+
if (routing) {
|
|
42
|
+
const word = MODE_WORDS[routing.mode] ?? routing.mode;
|
|
43
|
+
lines.push(
|
|
44
|
+
pc.dim(
|
|
45
|
+
routing.fellBack ? `ranked by ${word} time (straight-line estimate \u2014 routing engine unavailable)` : `ranked by ${word} time via ${routing.provider}`
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
lines.push("");
|
|
50
|
+
if (results.length === 0) {
|
|
51
|
+
lines.push(pc.dim("No amenities found within the search radius."));
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|
|
54
|
+
const rankWidth = String(results.length).length;
|
|
55
|
+
for (const poi of results) {
|
|
56
|
+
const rank = String(poi.rank).padStart(rankWidth);
|
|
57
|
+
const label = CATEGORY_LABELS[poi.category];
|
|
58
|
+
const name = poi.name ?? poi.kind ?? label;
|
|
59
|
+
const distance = formatDistance(poi.distanceMeters);
|
|
60
|
+
const metric = poi.travelSeconds !== void 0 ? `${formatDuration(poi.travelSeconds)} \xB7 ${distance}` : distance;
|
|
61
|
+
const meta = pc.dim(poi.rankingReason ?? `${label} \xB7 ${metric}`);
|
|
62
|
+
const tags = `${accessibilityMark(poi.tags.wheelchair)}${poi.rankingReason ? "" : openMark(poi)}`;
|
|
63
|
+
lines.push(`${pc.dim(`${rank}.`)} ${name} ${meta}${tags}`);
|
|
64
|
+
}
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
function accessibilityMark(wheelchair) {
|
|
68
|
+
if (wheelchair === "yes") return ` ${pc.green("\u267F")}`;
|
|
69
|
+
if (wheelchair === "limited") return ` ${pc.yellow("\u267F limited")}`;
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
var localHhmm = (iso) => {
|
|
73
|
+
const date = new Date(iso);
|
|
74
|
+
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
75
|
+
};
|
|
76
|
+
function openMark(poi) {
|
|
77
|
+
const state = poi.openState;
|
|
78
|
+
if (state === "open") {
|
|
79
|
+
const till = poi.nextChange ? ` ${pc.dim(`till ${localHhmm(poi.nextChange)}`)}` : "";
|
|
80
|
+
return ` ${pc.green("open")}${till}`;
|
|
81
|
+
}
|
|
82
|
+
if (state === "unknown") return ` ${pc.dim("hours?")}`;
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
function renderReachable(result) {
|
|
86
|
+
const { origin, results, withinMinutes, mode, isochrone, count } = result;
|
|
87
|
+
const word = MODE_WORDS[mode] ?? mode;
|
|
88
|
+
const basis = isochrone ? "isochrone" : "straight-line estimate";
|
|
89
|
+
const lines = [
|
|
90
|
+
pc.bold(origin.displayName),
|
|
91
|
+
pc.dim(
|
|
92
|
+
`${coordString(origin.location.lat, origin.location.lng)} \xB7 ${count} reachable within ${withinMinutes} min ${word} (${basis})`
|
|
93
|
+
),
|
|
94
|
+
""
|
|
95
|
+
];
|
|
96
|
+
if (results.length === 0) {
|
|
97
|
+
lines.push(pc.dim("Nothing reachable within that budget."));
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
const rankWidth = String(results.length).length;
|
|
101
|
+
for (const poi of results) {
|
|
102
|
+
const rank = String(poi.rank).padStart(rankWidth);
|
|
103
|
+
const label = CATEGORY_LABELS[poi.category];
|
|
104
|
+
const name = poi.name ?? poi.kind ?? label;
|
|
105
|
+
const distance = formatDistance(poi.distanceMeters);
|
|
106
|
+
const metric = poi.travelSeconds !== void 0 ? `${formatDuration(poi.travelSeconds)} \xB7 ${distance}` : distance;
|
|
107
|
+
lines.push(
|
|
108
|
+
`${pc.dim(`${rank}.`)} ${name} ${pc.dim(`${label} \xB7 ${metric}`)}${accessibilityMark(poi.tags.wheelchair)}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
function renderGeocode(places, ambiguous = false) {
|
|
114
|
+
if (places.length === 0) return "No matches found.";
|
|
115
|
+
const rankWidth = String(places.length).length;
|
|
116
|
+
const lines = [];
|
|
117
|
+
if (ambiguous) {
|
|
118
|
+
lines.push(
|
|
119
|
+
pc.yellow(`\u26A0 Ambiguous \u2014 ${places.length} distinct places match this name; pick one:`),
|
|
120
|
+
""
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
places.forEach((place, index) => {
|
|
124
|
+
const rank = String(index + 1).padStart(rankWidth);
|
|
125
|
+
lines.push(`${pc.dim(`${rank}.`)} ${pc.bold(place.name)}`);
|
|
126
|
+
lines.push(` ${pc.dim(place.displayName)}`);
|
|
127
|
+
lines.push(` ${pc.dim(coordString(place.location.lat, place.location.lng))}`);
|
|
128
|
+
});
|
|
129
|
+
return lines.join("\n");
|
|
130
|
+
}
|
|
131
|
+
function titleCase(value) {
|
|
132
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
133
|
+
}
|
|
134
|
+
function renderGaps(report) {
|
|
135
|
+
const { origin, gaps, missing, searchRadiusMeters, thresholdMeters } = report;
|
|
136
|
+
const lines = [
|
|
137
|
+
pc.bold(origin.displayName),
|
|
138
|
+
pc.dim(
|
|
139
|
+
`${coordString(origin.location.lat, origin.location.lng)} \xB7 searched ${formatDistance(searchRadiusMeters)}, gap if > ${formatDistance(thresholdMeters)}`
|
|
140
|
+
),
|
|
141
|
+
""
|
|
142
|
+
];
|
|
143
|
+
for (const gap of gaps) {
|
|
144
|
+
const mark = gap.isGap ? pc.red("\u2717") : pc.green("\u2713");
|
|
145
|
+
const detail = gap.nearestMeters === null ? pc.dim(`none within ${formatDistance(searchRadiusMeters)}`) : pc.dim(formatDistance(gap.nearestMeters));
|
|
146
|
+
lines.push(`${mark} ${titleCase(gap.category)} ${detail}`);
|
|
147
|
+
}
|
|
148
|
+
lines.push("");
|
|
149
|
+
lines.push(
|
|
150
|
+
missing.length === 0 ? pc.green("No gaps \u2014 all requested categories are nearby.") : pc.dim(`Missing (not found in OSM): ${missing.join(", ")}`)
|
|
151
|
+
);
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
function renderScore(report) {
|
|
155
|
+
const { origin, score, confidence, breakdown, missing, decay } = report;
|
|
156
|
+
const lines = [
|
|
157
|
+
pc.bold(origin.displayName),
|
|
158
|
+
pc.dim(
|
|
159
|
+
`${coordString(origin.location.lat, origin.location.lng)} \xB7 walkability ${pc.bold(String(score))}/100 \xB7 confidence ${Math.round(confidence * 100)}%`
|
|
160
|
+
),
|
|
161
|
+
pc.dim(
|
|
162
|
+
`full credit \u2264 ${formatDistance(decay.idealMeters)}, none \u2265 ${formatDistance(decay.maxMeters)}`
|
|
163
|
+
),
|
|
164
|
+
""
|
|
165
|
+
];
|
|
166
|
+
const nameWidth = Math.max(...breakdown.map((b) => titleCase(b.category).length));
|
|
167
|
+
for (const entry of breakdown) {
|
|
168
|
+
const mark = entry.subScore >= 0.67 ? pc.green("\u2713") : entry.subScore > 0 ? pc.yellow("~") : pc.red("\u2717");
|
|
169
|
+
const name = titleCase(entry.category).padEnd(nameWidth);
|
|
170
|
+
const detail = entry.nearestMeters === null ? pc.dim("none in range") : pc.dim(formatDistance(entry.nearestMeters));
|
|
171
|
+
const pct = pc.dim(`${Math.round(entry.subScore * 100)}%`.padStart(4));
|
|
172
|
+
lines.push(`${mark} ${name} ${pct} ${detail}`);
|
|
173
|
+
}
|
|
174
|
+
lines.push("");
|
|
175
|
+
lines.push(
|
|
176
|
+
missing.length === 0 ? pc.green("All daily needs reachable within range.") : pc.dim(`Not found in OSM within range: ${missing.join(", ")}`)
|
|
177
|
+
);
|
|
178
|
+
if (confidence < 0.5) {
|
|
179
|
+
lines.push(
|
|
180
|
+
pc.yellow("Low confidence \u2014 OSM coverage here looks sparse; treat the score as a floor.")
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return lines.join("\n");
|
|
184
|
+
}
|
|
185
|
+
function renderErrands(plan) {
|
|
186
|
+
const { origin, end, stops, totalSeconds, totalMeters, missing, mode } = plan;
|
|
187
|
+
const word = MODE_WORDS[mode] ?? mode;
|
|
188
|
+
const lines = [
|
|
189
|
+
pc.bold(`Errand plan from ${origin.displayName}`),
|
|
190
|
+
pc.dim(
|
|
191
|
+
`${stops.length} stop${stops.length === 1 ? "" : "s"} \xB7 ${formatDuration(totalSeconds)} ${word} \xB7 ${formatDistance(totalMeters)} (straight-line estimate)`
|
|
192
|
+
),
|
|
193
|
+
""
|
|
194
|
+
];
|
|
195
|
+
if (stops.length === 0) {
|
|
196
|
+
lines.push(pc.dim("No reachable stops for the requested categories."));
|
|
197
|
+
}
|
|
198
|
+
const stepWidth = String(stops.length).length;
|
|
199
|
+
stops.forEach((stop, index) => {
|
|
200
|
+
const step = String(index + 1).padStart(stepWidth);
|
|
201
|
+
const name = stop.poi.name ?? stop.poi.kind ?? stop.category;
|
|
202
|
+
const leg = pc.dim(`+${formatDuration(stop.legSeconds)} \xB7 ${formatDistance(stop.legMeters)}`);
|
|
203
|
+
lines.push(`${pc.dim(`${step}.`)} ${pc.bold(titleCase(stop.category))}: ${name} ${leg}`);
|
|
204
|
+
});
|
|
205
|
+
if (end) lines.push(pc.dim(` \u21B3 end: ${end.displayName}`));
|
|
206
|
+
if (missing.length > 0) {
|
|
207
|
+
lines.push("");
|
|
208
|
+
lines.push(pc.dim(`Not found nearby: ${missing.join(", ")}`));
|
|
209
|
+
}
|
|
210
|
+
return lines.join("\n");
|
|
211
|
+
}
|
|
212
|
+
function renderComparison(report) {
|
|
213
|
+
const weightStr = report.weights.map((w) => `${titleCase(w.term)}\xD7${w.weight}`).join(", ");
|
|
214
|
+
const lines = [pc.bold("Location comparison"), pc.dim(`weights: ${weightStr}`), ""];
|
|
215
|
+
const rankWidth = String(report.ranked.length).length;
|
|
216
|
+
report.ranked.forEach((entry, position) => {
|
|
217
|
+
const location = report.locations[entry.index];
|
|
218
|
+
const marker = position === 0 ? pc.green("\u2605") : " ";
|
|
219
|
+
const rank = String(position + 1).padStart(rankWidth);
|
|
220
|
+
lines.push(`${marker} ${pc.dim(`${rank}.`)} ${pc.bold(entry.origin.displayName)}`);
|
|
221
|
+
lines.push(
|
|
222
|
+
` ${pc.dim(`${entry.score}/100 \xB7 confidence ${Math.round(location.confidence * 100)}%`)}`
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
lines.push("");
|
|
226
|
+
lines.push(pc.bold("Best per category:"));
|
|
227
|
+
const nameWidth = Math.max(...report.dimensions.map((d) => titleCase(d.category).length));
|
|
228
|
+
for (const dimension of report.dimensions) {
|
|
229
|
+
const name = titleCase(dimension.category).padEnd(nameWidth);
|
|
230
|
+
let detail;
|
|
231
|
+
if (dimension.bestIndex === null) {
|
|
232
|
+
detail = pc.dim("none found");
|
|
233
|
+
} else {
|
|
234
|
+
const location = report.locations[dimension.bestIndex];
|
|
235
|
+
const entry = location.breakdown.find((b) => b.category === dimension.category);
|
|
236
|
+
const distance = entry && entry.nearestMeters !== null ? pc.dim(` (${formatDistance(entry.nearestMeters)})`) : "";
|
|
237
|
+
detail = `${location.origin.name}${distance}`;
|
|
238
|
+
}
|
|
239
|
+
lines.push(` ${name} ${detail}`);
|
|
240
|
+
}
|
|
241
|
+
return lines.join("\n");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/index.ts
|
|
245
|
+
var VERSION = "1.0.0";
|
|
246
|
+
function collect(value, previous) {
|
|
247
|
+
return [...previous, value];
|
|
248
|
+
}
|
|
249
|
+
function parsePositiveInt(value, name) {
|
|
250
|
+
const parsed = Number.parseInt(value, 10);
|
|
251
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
252
|
+
throw new Error(`--${name} must be a positive integer (got "${value}")`);
|
|
253
|
+
}
|
|
254
|
+
return parsed;
|
|
255
|
+
}
|
|
256
|
+
var FALSY = /* @__PURE__ */ new Set(["no", "false", "0", ""]);
|
|
257
|
+
var MODE_ALIASES = {
|
|
258
|
+
walk: "walk",
|
|
259
|
+
walking: "walk",
|
|
260
|
+
foot: "walk",
|
|
261
|
+
bike: "bike",
|
|
262
|
+
cycling: "bike",
|
|
263
|
+
bicycle: "bike",
|
|
264
|
+
drive: "drive",
|
|
265
|
+
driving: "drive",
|
|
266
|
+
car: "drive"
|
|
267
|
+
};
|
|
268
|
+
function parseMode(value) {
|
|
269
|
+
const mode = MODE_ALIASES[value.trim().toLowerCase()];
|
|
270
|
+
if (!mode) throw new Error(`--mode must be walk, bike, or drive (got "${value}")`);
|
|
271
|
+
return mode;
|
|
272
|
+
}
|
|
273
|
+
function parseWithin(value) {
|
|
274
|
+
const match = value.trim().toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(?:m|min|mins|minutes)?$/);
|
|
275
|
+
const minutes = match ? Number(match[1]) : NaN;
|
|
276
|
+
if (!Number.isFinite(minutes) || minutes <= 0) {
|
|
277
|
+
throw new Error(`--within must be a positive number of minutes (got "${value}")`);
|
|
278
|
+
}
|
|
279
|
+
return minutes;
|
|
280
|
+
}
|
|
281
|
+
function parseFilters(pairs) {
|
|
282
|
+
const filters = {};
|
|
283
|
+
const tags = {};
|
|
284
|
+
const push = (field, value) => {
|
|
285
|
+
if (!value) return;
|
|
286
|
+
const existing = filters[field];
|
|
287
|
+
filters[field] = existing ? [...Array.isArray(existing) ? existing : [existing], value] : value;
|
|
288
|
+
};
|
|
289
|
+
for (const pair of pairs) {
|
|
290
|
+
const eq = pair.indexOf("=");
|
|
291
|
+
const key = (eq === -1 ? pair : pair.slice(0, eq)).trim().toLowerCase();
|
|
292
|
+
const raw = eq === -1 ? "" : pair.slice(eq + 1).trim();
|
|
293
|
+
const bool = eq === -1 ? true : !FALSY.has(raw.toLowerCase());
|
|
294
|
+
switch (key) {
|
|
295
|
+
case "diet":
|
|
296
|
+
case "cuisine":
|
|
297
|
+
case "payment":
|
|
298
|
+
push(key, raw);
|
|
299
|
+
break;
|
|
300
|
+
case "wheelchair":
|
|
301
|
+
filters.wheelchair = raw || "yes";
|
|
302
|
+
break;
|
|
303
|
+
case "wifi":
|
|
304
|
+
case "internet":
|
|
305
|
+
case "internet_access":
|
|
306
|
+
filters.internetAccess = bool;
|
|
307
|
+
break;
|
|
308
|
+
case "takeaway":
|
|
309
|
+
filters.takeaway = bool;
|
|
310
|
+
break;
|
|
311
|
+
case "delivery":
|
|
312
|
+
filters.delivery = bool;
|
|
313
|
+
break;
|
|
314
|
+
case "outdoor":
|
|
315
|
+
case "outdoor_seating":
|
|
316
|
+
filters.outdoorSeating = bool;
|
|
317
|
+
break;
|
|
318
|
+
default:
|
|
319
|
+
tags[key] = eq === -1 ? true : raw;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (Object.keys(tags).length > 0) filters.tags = tags;
|
|
324
|
+
return filters;
|
|
325
|
+
}
|
|
326
|
+
function loadDataset(path) {
|
|
327
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
328
|
+
if (!data || typeof data !== "object" || !Array.isArray(data.pois)) {
|
|
329
|
+
throw new Error(`not a proximap snapshot (no "pois" array): ${path}`);
|
|
330
|
+
}
|
|
331
|
+
return data;
|
|
332
|
+
}
|
|
333
|
+
function csvField(value) {
|
|
334
|
+
return /[",\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
|
|
335
|
+
}
|
|
336
|
+
async function runNear(query, options) {
|
|
337
|
+
const radiusMeters = parsePositiveInt(options.radius, "radius");
|
|
338
|
+
const limit = parsePositiveInt(options.limit, "limit");
|
|
339
|
+
const filters = parseFilters(options.filter);
|
|
340
|
+
const open = options.openAt ? { at: options.openAt } : options.openNow ? "now" : void 0;
|
|
341
|
+
const byTravelTime = options.by !== void 0 && /time/i.test(options.by);
|
|
342
|
+
const places = options.dataset ? new DatasetPlacesProvider(loadDataset(options.dataset)) : void 0;
|
|
343
|
+
const result = await findNearbyAmenities(query, {
|
|
344
|
+
radiusMeters,
|
|
345
|
+
limit,
|
|
346
|
+
...places ? { places } : {},
|
|
347
|
+
...options.category.length > 0 ? { categories: options.category } : {},
|
|
348
|
+
...Object.keys(filters).length > 0 ? { filters } : {},
|
|
349
|
+
...options.accessible ? { accessible: true } : {},
|
|
350
|
+
...open ? { open } : {},
|
|
351
|
+
// Use the key-free public Valhalla engine for real road-network times; core
|
|
352
|
+
// falls back to straight-line estimates if it is unavailable.
|
|
353
|
+
...byTravelTime ? {
|
|
354
|
+
rankBy: "travelTime",
|
|
355
|
+
mode: parseMode(options.mode),
|
|
356
|
+
routing: new ValhallaRoutingProvider()
|
|
357
|
+
} : {},
|
|
358
|
+
...options.explain ? { explain: true } : {},
|
|
359
|
+
...options.lang ? { language: options.lang } : {}
|
|
360
|
+
});
|
|
361
|
+
if (options.format) {
|
|
362
|
+
const format = options.format.toLowerCase();
|
|
363
|
+
if (format === "geojson")
|
|
364
|
+
process.stdout.write(`${JSON.stringify(toGeoJSON(result), null, 2)}
|
|
365
|
+
`);
|
|
366
|
+
else if (format === "csv") process.stdout.write(`${toCSV(result)}
|
|
367
|
+
`);
|
|
368
|
+
else throw new Error(`unknown --format "${options.format}" (use geojson or csv)`);
|
|
369
|
+
process.stderr.write(`${ODBL_ATTRIBUTION}
|
|
370
|
+
`);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const output = options.json ? JSON.stringify(result, null, 2) : renderNearby(result);
|
|
374
|
+
process.stdout.write(`${output}
|
|
375
|
+
`);
|
|
376
|
+
}
|
|
377
|
+
async function runGeocode(query, options) {
|
|
378
|
+
const result = await disambiguateLocation(query, {
|
|
379
|
+
limit: parsePositiveInt(options.limit, "limit"),
|
|
380
|
+
...options.lang ? { language: options.lang } : {}
|
|
381
|
+
});
|
|
382
|
+
const output = options.json ? JSON.stringify(result, null, 2) : renderGeocode(result.candidates, result.ambiguous);
|
|
383
|
+
process.stdout.write(`${output}
|
|
384
|
+
`);
|
|
385
|
+
}
|
|
386
|
+
async function runGaps(query, options) {
|
|
387
|
+
const searchRadiusMeters = parsePositiveInt(options.radius, "radius");
|
|
388
|
+
const thresholdMeters = parsePositiveInt(options.threshold, "threshold");
|
|
389
|
+
const report = await detectGaps(query, {
|
|
390
|
+
searchRadiusMeters,
|
|
391
|
+
thresholdMeters,
|
|
392
|
+
...options.category.length > 0 ? { categories: options.category } : {},
|
|
393
|
+
...options.lang ? { language: options.lang } : {}
|
|
394
|
+
});
|
|
395
|
+
const output = options.json ? JSON.stringify(report, null, 2) : renderGaps(report);
|
|
396
|
+
process.stdout.write(`${output}
|
|
397
|
+
`);
|
|
398
|
+
}
|
|
399
|
+
async function runScore(query, options) {
|
|
400
|
+
const idealMeters = parsePositiveInt(options.ideal, "ideal");
|
|
401
|
+
const maxMeters = parsePositiveInt(options.max, "max");
|
|
402
|
+
if (maxMeters <= idealMeters) {
|
|
403
|
+
throw new Error("--max must be greater than --ideal");
|
|
404
|
+
}
|
|
405
|
+
const report = await walkabilityScore(query, {
|
|
406
|
+
decay: { idealMeters, maxMeters },
|
|
407
|
+
...options.lang ? { language: options.lang } : {}
|
|
408
|
+
});
|
|
409
|
+
const output = options.json ? JSON.stringify(report, null, 2) : renderScore(report);
|
|
410
|
+
process.stdout.write(`${output}
|
|
411
|
+
`);
|
|
412
|
+
}
|
|
413
|
+
function parseWeights(spec) {
|
|
414
|
+
const weights = [];
|
|
415
|
+
for (const part of spec.split(",")) {
|
|
416
|
+
const [term, value] = part.split("=");
|
|
417
|
+
const name = (term ?? "").trim();
|
|
418
|
+
const weight = Number(value);
|
|
419
|
+
if (!name || !Number.isFinite(weight) || weight <= 0) {
|
|
420
|
+
throw new Error(`invalid --weights entry: "${part}" (use term=weight, e.g. food=2)`);
|
|
421
|
+
}
|
|
422
|
+
weights.push({ term: name, weight });
|
|
423
|
+
}
|
|
424
|
+
if (weights.length === 0) throw new Error("--weights needs at least one term=weight");
|
|
425
|
+
return weights;
|
|
426
|
+
}
|
|
427
|
+
async function runCompare(queries, options) {
|
|
428
|
+
const idealMeters = parsePositiveInt(options.ideal, "ideal");
|
|
429
|
+
const maxMeters = parsePositiveInt(options.max, "max");
|
|
430
|
+
if (maxMeters <= idealMeters) {
|
|
431
|
+
throw new Error("--max must be greater than --ideal");
|
|
432
|
+
}
|
|
433
|
+
const categories = options.weights ? parseWeights(options.weights) : void 0;
|
|
434
|
+
const report = await compareLocations(queries, {
|
|
435
|
+
decay: { idealMeters, maxMeters },
|
|
436
|
+
...categories ? { categories } : {},
|
|
437
|
+
...options.lang ? { language: options.lang } : {}
|
|
438
|
+
});
|
|
439
|
+
const output = options.json ? JSON.stringify(report, null, 2) : renderComparison(report);
|
|
440
|
+
process.stdout.write(`${output}
|
|
441
|
+
`);
|
|
442
|
+
}
|
|
443
|
+
async function runReachable(query, options) {
|
|
444
|
+
const result = await reachableAmenities(query, {
|
|
445
|
+
within: parseWithin(options.within),
|
|
446
|
+
mode: parseMode(options.mode),
|
|
447
|
+
routing: new ValhallaRoutingProvider(),
|
|
448
|
+
...options.category.length > 0 ? { categories: options.category } : {},
|
|
449
|
+
...options.lang ? { language: options.lang } : {}
|
|
450
|
+
});
|
|
451
|
+
const output = options.json ? JSON.stringify(result, null, 2) : renderReachable(result);
|
|
452
|
+
process.stdout.write(`${output}
|
|
453
|
+
`);
|
|
454
|
+
}
|
|
455
|
+
async function runErrands(query, options) {
|
|
456
|
+
if (options.category.length === 0) {
|
|
457
|
+
throw new Error("errands needs at least one -c/--category");
|
|
458
|
+
}
|
|
459
|
+
const plan = await planErrands(query, {
|
|
460
|
+
categories: options.category,
|
|
461
|
+
mode: parseMode(options.mode),
|
|
462
|
+
candidatesPerCategory: parsePositiveInt(options.candidates, "candidates"),
|
|
463
|
+
searchRadiusMeters: parsePositiveInt(options.radius, "radius"),
|
|
464
|
+
...options.end ? { end: options.end } : {},
|
|
465
|
+
...options.lang ? { language: options.lang } : {}
|
|
466
|
+
});
|
|
467
|
+
const output = options.json ? JSON.stringify(plan, null, 2) : renderErrands(plan);
|
|
468
|
+
process.stdout.write(`${output}
|
|
469
|
+
`);
|
|
470
|
+
}
|
|
471
|
+
async function runSnapshot(query, options) {
|
|
472
|
+
const dataset = await snapshotArea(query, {
|
|
473
|
+
radiusMeters: parsePositiveInt(options.radius, "radius"),
|
|
474
|
+
...options.category.length > 0 ? { categories: options.category } : {},
|
|
475
|
+
...options.lang ? { language: options.lang } : {}
|
|
476
|
+
});
|
|
477
|
+
const json = JSON.stringify(dataset, null, 2);
|
|
478
|
+
if (options.out) {
|
|
479
|
+
writeFileSync(options.out, `${json}
|
|
480
|
+
`, "utf8");
|
|
481
|
+
process.stderr.write(`Wrote ${dataset.pois.length} POIs to ${options.out}
|
|
482
|
+
`);
|
|
483
|
+
} else {
|
|
484
|
+
process.stdout.write(`${json}
|
|
485
|
+
`);
|
|
486
|
+
}
|
|
487
|
+
process.stderr.write(`${ODBL_ATTRIBUTION}
|
|
488
|
+
`);
|
|
489
|
+
}
|
|
490
|
+
async function runBulk(file, options) {
|
|
491
|
+
const idealMeters = parsePositiveInt(options.ideal, "ideal");
|
|
492
|
+
const maxMeters = parsePositiveInt(options.max, "max");
|
|
493
|
+
if (maxMeters <= idealMeters) throw new Error("--max must be greater than --ideal");
|
|
494
|
+
const locations = readFileSync(file, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
495
|
+
process.stdout.write("location,lat,lng,score,confidence,missing\n");
|
|
496
|
+
for (const location of locations) {
|
|
497
|
+
try {
|
|
498
|
+
const report = await walkabilityScore(location, {
|
|
499
|
+
decay: { idealMeters, maxMeters },
|
|
500
|
+
...options.lang ? { language: options.lang } : {}
|
|
501
|
+
});
|
|
502
|
+
const { lat, lng } = report.origin.location;
|
|
503
|
+
process.stdout.write(
|
|
504
|
+
`${csvField(location)},${lat},${lng},${report.score},${report.confidence},${csvField(report.missing.join(";"))}
|
|
505
|
+
`
|
|
506
|
+
);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
509
|
+
process.stdout.write(`${csvField(location)},,,,,${csvField(`error: ${message}`)}
|
|
510
|
+
`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
process.stderr.write(`${ODBL_ATTRIBUTION}
|
|
514
|
+
`);
|
|
515
|
+
}
|
|
516
|
+
function fail(error) {
|
|
517
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
518
|
+
process.stderr.write(`proximap: ${message}
|
|
519
|
+
`);
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
var program = new Command();
|
|
523
|
+
program.name("proximap").description("Find and rank what is near any place \u2014 powered by OpenStreetMap.").version(VERSION).showHelpAfterError("(run with --help for usage)");
|
|
524
|
+
program.command("near").description("list nearby amenities ranked by distance").argument("<query...>", 'place name, address, or "lat,lng"').option("-r, --radius <meters>", "search radius in metres", "1000").option(
|
|
525
|
+
"-c, --category <term>",
|
|
526
|
+
"restrict to a category or term, e.g. coffee (repeatable)",
|
|
527
|
+
collect,
|
|
528
|
+
[]
|
|
529
|
+
).option(
|
|
530
|
+
"-f, --filter <key=value>",
|
|
531
|
+
"facet filter, e.g. diet=vegan, payment=contactless, wifi (repeatable)",
|
|
532
|
+
collect,
|
|
533
|
+
[]
|
|
534
|
+
).option("--accessible", "rank step-free / wheelchair-accessible places first").option("--open-now", "keep only places open right now (unknown hours kept, labelled)").option("--open-at <when>", "keep only places open at an ISO time, e.g. 2026-06-20T21:00").option("--by <metric>", "rank by distance (default) or travel-time").option("--mode <mode>", "travel mode for --by travel-time: walk, bike, drive", "walk").option("--explain", "annotate each result with a short ranking reason").option(
|
|
535
|
+
"--dataset <file>",
|
|
536
|
+
'query a local snapshot file offline (use "lat,lng" for full offline)'
|
|
537
|
+
).option("-n, --limit <count>", "maximum number of results", "20").option("--lang <code>", "preferred language for place names (e.g. en)").option("--json", "output raw JSON instead of a list").option("--format <type>", "export results as geojson or csv (ODbL notice on stderr)").action((parts, options) => runNear(parts.join(" "), options));
|
|
538
|
+
program.command("geocode").description("resolve a place name to coordinates").argument("<query...>", "place name or address").option("-n, --limit <count>", "maximum number of candidates", "5").option("--lang <code>", "preferred language").option("--json", "output raw JSON").action(
|
|
539
|
+
(parts, options) => runGeocode(parts.join(" "), options)
|
|
540
|
+
);
|
|
541
|
+
program.command("gaps").description("report which everyday amenities are missing near a place").argument("<query...>", 'place name, address, or "lat,lng"').option(
|
|
542
|
+
"-c, --category <term>",
|
|
543
|
+
"category to check (repeatable; default: daily needs)",
|
|
544
|
+
collect,
|
|
545
|
+
[]
|
|
546
|
+
).option("-r, --radius <meters>", "how far to search for the nearest match", "5000").option("-t, --threshold <meters>", "distance beyond which a category is a gap", "1500").option("--lang <code>", "preferred language").option("--json", "output raw JSON").action((parts, options) => runGaps(parts.join(" "), options));
|
|
547
|
+
program.command("score").description("rate how walkable / well-served a place is (0-100, with a breakdown)").argument("<query...>", 'place name, address, or "lat,lng"').option("--ideal <meters>", "distance that still scores full marks (\u22485-min walk)", "400").option("--max <meters>", "distance beyond which a category scores zero (\u224830-min walk)", "2400").option("--lang <code>", "preferred language").option("--json", "output raw JSON").action((parts, options) => runScore(parts.join(" "), options));
|
|
548
|
+
program.command("reachable").description("list amenities reachable within a time budget (isochrone)").argument("<query...>", 'place name, address, or "lat,lng"').option("--within <minutes>", "time budget, e.g. 15 or 15min", "15").option("--mode <mode>", "travel mode: walk, bike, drive", "walk").option("-c, --category <term>", "restrict to a category or term (repeatable)", collect, []).option("--lang <code>", "preferred language").option("--json", "output raw JSON").action(
|
|
549
|
+
(parts, options) => runReachable(parts.join(" "), options)
|
|
550
|
+
);
|
|
551
|
+
program.command("errands").description("plan the shortest trip that hits one of each category").argument("<query...>", 'starting place name, address, or "lat,lng"').option("-c, --category <term>", "a category to hit one of (repeatable, required)", collect, []).option("--mode <mode>", "travel mode: walk, bike, drive", "walk").option("--end <place>", "optional fixed end point").option("--candidates <count>", "nearest candidates considered per category", "5").option("-r, --radius <meters>", "how far to look for candidates", "3000").option("--lang <code>", "preferred language").option("--json", "output raw JSON").action(
|
|
552
|
+
(parts, options) => runErrands(parts.join(" "), options)
|
|
553
|
+
);
|
|
554
|
+
program.command("compare").description("compare 2+ locations by access to what you care about").argument("<locations...>", 'two or more quoted place names, addresses, or "lat,lng"').option(
|
|
555
|
+
"-w, --weights <list>",
|
|
556
|
+
"comma list term=weight, e.g. food=2,transport=3 (default: daily needs)"
|
|
557
|
+
).option("--ideal <meters>", "distance that still scores full marks", "400").option("--max <meters>", "distance beyond which a category scores zero", "2400").option("--lang <code>", "preferred language").option("--json", "output raw JSON").action((locations, options) => runCompare(locations, options));
|
|
558
|
+
program.command("snapshot").description("capture an area's POIs to a file for offline reuse (ODbL \u2014 yours to store)").argument("<query...>", 'area center: place name, address, or "lat,lng"').option("-o, --out <file>", "write the snapshot JSON to this file (else stdout)").option("-r, --radius <meters>", "radius to capture", "2000").option(
|
|
559
|
+
"-c, --category <term>",
|
|
560
|
+
"restrict the capture to a category/term (repeatable)",
|
|
561
|
+
collect,
|
|
562
|
+
[]
|
|
563
|
+
).option("--lang <code>", "preferred language").action(
|
|
564
|
+
(parts, options) => runSnapshot(parts.join(" "), options)
|
|
565
|
+
);
|
|
566
|
+
program.command("bulk").description("walkability-score many locations from a file (one per line) \u2192 CSV").argument("<file>", 'text file with one place/address/"lat,lng" per line (# comments allowed)').option("--ideal <meters>", "distance that still scores full marks", "400").option("--max <meters>", "distance beyond which a category scores zero", "2400").option("--lang <code>", "preferred language").action((file, options) => runBulk(file, options));
|
|
567
|
+
program.parseAsync().catch(fail);
|
|
568
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/render.ts"],"sourcesContent":["import { readFileSync, writeFileSync } from 'node:fs';\nimport {\n compareLocations,\n DatasetPlacesProvider,\n detectGaps,\n disambiguateLocation,\n findNearbyAmenities,\n ODBL_ATTRIBUTION,\n planErrands,\n reachableAmenities,\n snapshotArea,\n toCSV,\n toGeoJSON,\n ValhallaRoutingProvider,\n walkabilityScore,\n type CategoryWeight,\n type FacetFilters,\n type SnapshotDataset,\n type TravelMode,\n} from '@proximap/core';\nimport { Command } from 'commander';\nimport {\n renderComparison,\n renderErrands,\n renderGaps,\n renderGeocode,\n renderNearby,\n renderReachable,\n renderScore,\n} from './render';\n\nconst VERSION = '1.0.0';\n\ninterface NearOptions {\n radius: string;\n category: string[];\n filter: string[];\n accessible?: boolean;\n openNow?: boolean;\n openAt?: string;\n by?: string;\n mode: string;\n explain?: boolean;\n dataset?: string;\n limit: string;\n lang?: string;\n json?: boolean;\n format?: string;\n}\n\ninterface SnapshotCommandOptions {\n out?: string;\n radius: string;\n category: string[];\n lang?: string;\n}\n\ninterface BulkCommandOptions {\n ideal: string;\n max: string;\n lang?: string;\n}\n\ninterface GeocodeCommandOptions {\n limit: string;\n lang?: string;\n json?: boolean;\n}\n\ninterface GapsCommandOptions {\n category: string[];\n radius: string;\n threshold: string;\n lang?: string;\n json?: boolean;\n}\n\ninterface ScoreCommandOptions {\n ideal: string;\n max: string;\n lang?: string;\n json?: boolean;\n}\n\ninterface CompareCommandOptions {\n weights?: string;\n ideal: string;\n max: string;\n lang?: string;\n json?: boolean;\n}\n\ninterface ReachableCommandOptions {\n within: string;\n mode: string;\n category: string[];\n lang?: string;\n json?: boolean;\n}\n\ninterface ErrandsCommandOptions {\n category: string[];\n mode: string;\n end?: string;\n candidates: string;\n radius: string;\n lang?: string;\n json?: boolean;\n}\n\nfunction collect(value: string, previous: string[]): string[] {\n return [...previous, value];\n}\n\nfunction parsePositiveInt(value: string, name: string): number {\n const parsed = Number.parseInt(value, 10);\n if (!Number.isFinite(parsed) || parsed <= 0) {\n throw new Error(`--${name} must be a positive integer (got \"${value}\")`);\n }\n return parsed;\n}\n\nconst FALSY = new Set(['no', 'false', '0', '']);\n\nconst MODE_ALIASES: Record<string, TravelMode> = {\n walk: 'walk',\n walking: 'walk',\n foot: 'walk',\n bike: 'bike',\n cycling: 'bike',\n bicycle: 'bike',\n drive: 'drive',\n driving: 'drive',\n car: 'drive',\n};\n\nfunction parseMode(value: string): TravelMode {\n const mode = MODE_ALIASES[value.trim().toLowerCase()];\n if (!mode) throw new Error(`--mode must be walk, bike, or drive (got \"${value}\")`);\n return mode;\n}\n\n/** Parse a time budget like \"15\", \"15min\", or \"15 minutes\" into minutes. */\nfunction parseWithin(value: string): number {\n const match = value\n .trim()\n .toLowerCase()\n .match(/^(\\d+(?:\\.\\d+)?)\\s*(?:m|min|mins|minutes)?$/);\n const minutes = match ? Number(match[1]) : NaN;\n if (!Number.isFinite(minutes) || minutes <= 0) {\n throw new Error(`--within must be a positive number of minutes (got \"${value}\")`);\n }\n return minutes;\n}\n\n/** Parse repeatable `--filter key=value` (or bare `key`) pairs into FacetFilters. */\nfunction parseFilters(pairs: string[]): FacetFilters {\n const filters: FacetFilters = {};\n const tags: Record<string, string | boolean> = {};\n const push = (field: 'diet' | 'cuisine' | 'payment', value: string): void => {\n if (!value) return;\n const existing = filters[field];\n filters[field] = existing\n ? [...(Array.isArray(existing) ? existing : [existing]), value]\n : value;\n };\n\n for (const pair of pairs) {\n const eq = pair.indexOf('=');\n const key = (eq === -1 ? pair : pair.slice(0, eq)).trim().toLowerCase();\n const raw = eq === -1 ? '' : pair.slice(eq + 1).trim();\n const bool = eq === -1 ? true : !FALSY.has(raw.toLowerCase());\n\n switch (key) {\n case 'diet':\n case 'cuisine':\n case 'payment':\n push(key, raw);\n break;\n case 'wheelchair':\n filters.wheelchair = raw || 'yes';\n break;\n case 'wifi':\n case 'internet':\n case 'internet_access':\n filters.internetAccess = bool;\n break;\n case 'takeaway':\n filters.takeaway = bool;\n break;\n case 'delivery':\n filters.delivery = bool;\n break;\n case 'outdoor':\n case 'outdoor_seating':\n filters.outdoorSeating = bool;\n break;\n default:\n tags[key] = eq === -1 ? true : raw;\n break;\n }\n }\n if (Object.keys(tags).length > 0) filters.tags = tags;\n return filters;\n}\n\n/** Load a proximap snapshot file for offline POI queries. */\nfunction loadDataset(path: string): SnapshotDataset {\n const data: unknown = JSON.parse(readFileSync(path, 'utf8'));\n if (!data || typeof data !== 'object' || !Array.isArray((data as SnapshotDataset).pois)) {\n throw new Error(`not a proximap snapshot (no \"pois\" array): ${path}`);\n }\n return data as SnapshotDataset;\n}\n\n/** Quote a CSV field when it contains a comma, quote, or newline (RFC 4180). */\nfunction csvField(value: string): string {\n return /[\",\\n\\r]/.test(value) ? `\"${value.replace(/\"/g, '\"\"')}\"` : value;\n}\n\nasync function runNear(query: string, options: NearOptions): Promise<void> {\n const radiusMeters = parsePositiveInt(options.radius, 'radius');\n const limit = parsePositiveInt(options.limit, 'limit');\n\n const filters = parseFilters(options.filter);\n const open = options.openAt ? { at: options.openAt } : options.openNow ? 'now' : undefined;\n const byTravelTime = options.by !== undefined && /time/i.test(options.by);\n const places = options.dataset\n ? new DatasetPlacesProvider(loadDataset(options.dataset))\n : undefined;\n const result = await findNearbyAmenities(query, {\n radiusMeters,\n limit,\n ...(places ? { places } : {}),\n ...(options.category.length > 0 ? { categories: options.category } : {}),\n ...(Object.keys(filters).length > 0 ? { filters } : {}),\n ...(options.accessible ? { accessible: true } : {}),\n ...(open ? { open } : {}),\n // Use the key-free public Valhalla engine for real road-network times; core\n // falls back to straight-line estimates if it is unavailable.\n ...(byTravelTime\n ? {\n rankBy: 'travelTime' as const,\n mode: parseMode(options.mode),\n routing: new ValhallaRoutingProvider(),\n }\n : {}),\n ...(options.explain ? { explain: true } : {}),\n ...(options.lang ? { language: options.lang } : {}),\n });\n\n if (options.format) {\n const format = options.format.toLowerCase();\n if (format === 'geojson')\n process.stdout.write(`${JSON.stringify(toGeoJSON(result), null, 2)}\\n`);\n else if (format === 'csv') process.stdout.write(`${toCSV(result)}\\n`);\n else throw new Error(`unknown --format \"${options.format}\" (use geojson or csv)`);\n // Keep the ODbL notice off stdout so the data stays pipeable to a file.\n process.stderr.write(`${ODBL_ATTRIBUTION}\\n`);\n return;\n }\n\n const output = options.json ? JSON.stringify(result, null, 2) : renderNearby(result);\n process.stdout.write(`${output}\\n`);\n}\n\nasync function runGeocode(query: string, options: GeocodeCommandOptions): Promise<void> {\n const result = await disambiguateLocation(query, {\n limit: parsePositiveInt(options.limit, 'limit'),\n ...(options.lang ? { language: options.lang } : {}),\n });\n const output = options.json\n ? JSON.stringify(result, null, 2)\n : renderGeocode(result.candidates, result.ambiguous);\n process.stdout.write(`${output}\\n`);\n}\n\nasync function runGaps(query: string, options: GapsCommandOptions): Promise<void> {\n const searchRadiusMeters = parsePositiveInt(options.radius, 'radius');\n const thresholdMeters = parsePositiveInt(options.threshold, 'threshold');\n const report = await detectGaps(query, {\n searchRadiusMeters,\n thresholdMeters,\n ...(options.category.length > 0 ? { categories: options.category } : {}),\n ...(options.lang ? { language: options.lang } : {}),\n });\n const output = options.json ? JSON.stringify(report, null, 2) : renderGaps(report);\n process.stdout.write(`${output}\\n`);\n}\n\nasync function runScore(query: string, options: ScoreCommandOptions): Promise<void> {\n const idealMeters = parsePositiveInt(options.ideal, 'ideal');\n const maxMeters = parsePositiveInt(options.max, 'max');\n if (maxMeters <= idealMeters) {\n throw new Error('--max must be greater than --ideal');\n }\n const report = await walkabilityScore(query, {\n decay: { idealMeters, maxMeters },\n ...(options.lang ? { language: options.lang } : {}),\n });\n const output = options.json ? JSON.stringify(report, null, 2) : renderScore(report);\n process.stdout.write(`${output}\\n`);\n}\n\n/** Parse `term=weight,term=weight` into CategoryWeight[]. */\nfunction parseWeights(spec: string): CategoryWeight[] {\n const weights: CategoryWeight[] = [];\n for (const part of spec.split(',')) {\n const [term, value] = part.split('=');\n const name = (term ?? '').trim();\n const weight = Number(value);\n if (!name || !Number.isFinite(weight) || weight <= 0) {\n throw new Error(`invalid --weights entry: \"${part}\" (use term=weight, e.g. food=2)`);\n }\n weights.push({ term: name, weight });\n }\n if (weights.length === 0) throw new Error('--weights needs at least one term=weight');\n return weights;\n}\n\nasync function runCompare(queries: string[], options: CompareCommandOptions): Promise<void> {\n const idealMeters = parsePositiveInt(options.ideal, 'ideal');\n const maxMeters = parsePositiveInt(options.max, 'max');\n if (maxMeters <= idealMeters) {\n throw new Error('--max must be greater than --ideal');\n }\n const categories = options.weights ? parseWeights(options.weights) : undefined;\n const report = await compareLocations(queries, {\n decay: { idealMeters, maxMeters },\n ...(categories ? { categories } : {}),\n ...(options.lang ? { language: options.lang } : {}),\n });\n const output = options.json ? JSON.stringify(report, null, 2) : renderComparison(report);\n process.stdout.write(`${output}\\n`);\n}\n\nasync function runReachable(query: string, options: ReachableCommandOptions): Promise<void> {\n const result = await reachableAmenities(query, {\n within: parseWithin(options.within),\n mode: parseMode(options.mode),\n routing: new ValhallaRoutingProvider(),\n ...(options.category.length > 0 ? { categories: options.category } : {}),\n ...(options.lang ? { language: options.lang } : {}),\n });\n const output = options.json ? JSON.stringify(result, null, 2) : renderReachable(result);\n process.stdout.write(`${output}\\n`);\n}\n\nasync function runErrands(query: string, options: ErrandsCommandOptions): Promise<void> {\n if (options.category.length === 0) {\n throw new Error('errands needs at least one -c/--category');\n }\n const plan = await planErrands(query, {\n categories: options.category,\n mode: parseMode(options.mode),\n candidatesPerCategory: parsePositiveInt(options.candidates, 'candidates'),\n searchRadiusMeters: parsePositiveInt(options.radius, 'radius'),\n ...(options.end ? { end: options.end } : {}),\n ...(options.lang ? { language: options.lang } : {}),\n });\n const output = options.json ? JSON.stringify(plan, null, 2) : renderErrands(plan);\n process.stdout.write(`${output}\\n`);\n}\n\nasync function runSnapshot(query: string, options: SnapshotCommandOptions): Promise<void> {\n const dataset = await snapshotArea(query, {\n radiusMeters: parsePositiveInt(options.radius, 'radius'),\n ...(options.category.length > 0 ? { categories: options.category } : {}),\n ...(options.lang ? { language: options.lang } : {}),\n });\n const json = JSON.stringify(dataset, null, 2);\n if (options.out) {\n writeFileSync(options.out, `${json}\\n`, 'utf8');\n process.stderr.write(`Wrote ${dataset.pois.length} POIs to ${options.out}\\n`);\n } else {\n process.stdout.write(`${json}\\n`);\n }\n process.stderr.write(`${ODBL_ATTRIBUTION}\\n`);\n}\n\nasync function runBulk(file: string, options: BulkCommandOptions): Promise<void> {\n const idealMeters = parsePositiveInt(options.ideal, 'ideal');\n const maxMeters = parsePositiveInt(options.max, 'max');\n if (maxMeters <= idealMeters) throw new Error('--max must be greater than --ideal');\n\n const locations = readFileSync(file, 'utf8')\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter((line) => line.length > 0 && !line.startsWith('#'));\n\n process.stdout.write('location,lat,lng,score,confidence,missing\\n');\n for (const location of locations) {\n try {\n const report = await walkabilityScore(location, {\n decay: { idealMeters, maxMeters },\n ...(options.lang ? { language: options.lang } : {}),\n });\n const { lat, lng } = report.origin.location;\n process.stdout.write(\n `${csvField(location)},${lat},${lng},${report.score},${report.confidence},${csvField(report.missing.join(';'))}\\n`,\n );\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n process.stdout.write(`${csvField(location)},,,,,${csvField(`error: ${message}`)}\\n`);\n }\n }\n process.stderr.write(`${ODBL_ATTRIBUTION}\\n`);\n}\n\nfunction fail(error: unknown): never {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`proximap: ${message}\\n`);\n process.exit(1);\n}\n\nconst program = new Command();\nprogram\n .name('proximap')\n .description('Find and rank what is near any place — powered by OpenStreetMap.')\n .version(VERSION)\n .showHelpAfterError('(run with --help for usage)');\n\nprogram\n .command('near')\n .description('list nearby amenities ranked by distance')\n .argument('<query...>', 'place name, address, or \"lat,lng\"')\n .option('-r, --radius <meters>', 'search radius in metres', '1000')\n .option(\n '-c, --category <term>',\n 'restrict to a category or term, e.g. coffee (repeatable)',\n collect,\n [],\n )\n .option(\n '-f, --filter <key=value>',\n 'facet filter, e.g. diet=vegan, payment=contactless, wifi (repeatable)',\n collect,\n [],\n )\n .option('--accessible', 'rank step-free / wheelchair-accessible places first')\n .option('--open-now', 'keep only places open right now (unknown hours kept, labelled)')\n .option('--open-at <when>', 'keep only places open at an ISO time, e.g. 2026-06-20T21:00')\n .option('--by <metric>', 'rank by distance (default) or travel-time')\n .option('--mode <mode>', 'travel mode for --by travel-time: walk, bike, drive', 'walk')\n .option('--explain', 'annotate each result with a short ranking reason')\n .option(\n '--dataset <file>',\n 'query a local snapshot file offline (use \"lat,lng\" for full offline)',\n )\n .option('-n, --limit <count>', 'maximum number of results', '20')\n .option('--lang <code>', 'preferred language for place names (e.g. en)')\n .option('--json', 'output raw JSON instead of a list')\n .option('--format <type>', 'export results as geojson or csv (ODbL notice on stderr)')\n .action((parts: string[], options: NearOptions) => runNear(parts.join(' '), options));\n\nprogram\n .command('geocode')\n .description('resolve a place name to coordinates')\n .argument('<query...>', 'place name or address')\n .option('-n, --limit <count>', 'maximum number of candidates', '5')\n .option('--lang <code>', 'preferred language')\n .option('--json', 'output raw JSON')\n .action((parts: string[], options: GeocodeCommandOptions) =>\n runGeocode(parts.join(' '), options),\n );\n\nprogram\n .command('gaps')\n .description('report which everyday amenities are missing near a place')\n .argument('<query...>', 'place name, address, or \"lat,lng\"')\n .option(\n '-c, --category <term>',\n 'category to check (repeatable; default: daily needs)',\n collect,\n [],\n )\n .option('-r, --radius <meters>', 'how far to search for the nearest match', '5000')\n .option('-t, --threshold <meters>', 'distance beyond which a category is a gap', '1500')\n .option('--lang <code>', 'preferred language')\n .option('--json', 'output raw JSON')\n .action((parts: string[], options: GapsCommandOptions) => runGaps(parts.join(' '), options));\n\nprogram\n .command('score')\n .description('rate how walkable / well-served a place is (0-100, with a breakdown)')\n .argument('<query...>', 'place name, address, or \"lat,lng\"')\n .option('--ideal <meters>', 'distance that still scores full marks (≈5-min walk)', '400')\n .option('--max <meters>', 'distance beyond which a category scores zero (≈30-min walk)', '2400')\n .option('--lang <code>', 'preferred language')\n .option('--json', 'output raw JSON')\n .action((parts: string[], options: ScoreCommandOptions) => runScore(parts.join(' '), options));\n\nprogram\n .command('reachable')\n .description('list amenities reachable within a time budget (isochrone)')\n .argument('<query...>', 'place name, address, or \"lat,lng\"')\n .option('--within <minutes>', 'time budget, e.g. 15 or 15min', '15')\n .option('--mode <mode>', 'travel mode: walk, bike, drive', 'walk')\n .option('-c, --category <term>', 'restrict to a category or term (repeatable)', collect, [])\n .option('--lang <code>', 'preferred language')\n .option('--json', 'output raw JSON')\n .action((parts: string[], options: ReachableCommandOptions) =>\n runReachable(parts.join(' '), options),\n );\n\nprogram\n .command('errands')\n .description('plan the shortest trip that hits one of each category')\n .argument('<query...>', 'starting place name, address, or \"lat,lng\"')\n .option('-c, --category <term>', 'a category to hit one of (repeatable, required)', collect, [])\n .option('--mode <mode>', 'travel mode: walk, bike, drive', 'walk')\n .option('--end <place>', 'optional fixed end point')\n .option('--candidates <count>', 'nearest candidates considered per category', '5')\n .option('-r, --radius <meters>', 'how far to look for candidates', '3000')\n .option('--lang <code>', 'preferred language')\n .option('--json', 'output raw JSON')\n .action((parts: string[], options: ErrandsCommandOptions) =>\n runErrands(parts.join(' '), options),\n );\n\nprogram\n .command('compare')\n .description('compare 2+ locations by access to what you care about')\n .argument('<locations...>', 'two or more quoted place names, addresses, or \"lat,lng\"')\n .option(\n '-w, --weights <list>',\n 'comma list term=weight, e.g. food=2,transport=3 (default: daily needs)',\n )\n .option('--ideal <meters>', 'distance that still scores full marks', '400')\n .option('--max <meters>', 'distance beyond which a category scores zero', '2400')\n .option('--lang <code>', 'preferred language')\n .option('--json', 'output raw JSON')\n .action((locations: string[], options: CompareCommandOptions) => runCompare(locations, options));\n\nprogram\n .command('snapshot')\n .description(\"capture an area's POIs to a file for offline reuse (ODbL — yours to store)\")\n .argument('<query...>', 'area center: place name, address, or \"lat,lng\"')\n .option('-o, --out <file>', 'write the snapshot JSON to this file (else stdout)')\n .option('-r, --radius <meters>', 'radius to capture', '2000')\n .option(\n '-c, --category <term>',\n 'restrict the capture to a category/term (repeatable)',\n collect,\n [],\n )\n .option('--lang <code>', 'preferred language')\n .action((parts: string[], options: SnapshotCommandOptions) =>\n runSnapshot(parts.join(' '), options),\n );\n\nprogram\n .command('bulk')\n .description('walkability-score many locations from a file (one per line) → CSV')\n .argument('<file>', 'text file with one place/address/\"lat,lng\" per line (# comments allowed)')\n .option('--ideal <meters>', 'distance that still scores full marks', '400')\n .option('--max <meters>', 'distance beyond which a category scores zero', '2400')\n .option('--lang <code>', 'preferred language')\n .action((file: string, options: BulkCommandOptions) => runBulk(file, options));\n\nprogram.parseAsync().catch(fail);\n","import {\n CATEGORY_LABELS,\n formatDistance,\n formatDuration,\n type ComparisonReport,\n type ErrandPlan,\n type GapReport,\n type NearbyResult,\n type OpenState,\n type Place,\n type RankedPoi,\n type ReachableResult,\n type WalkabilityReport,\n} from '@proximap/core';\nimport pc from 'picocolors';\n\nconst MODE_WORDS: Record<string, string> = { walk: 'walking', bike: 'cycling', drive: 'driving' };\n\nfunction coordString(lat: number, lng: number): string {\n return `${lat.toFixed(5)}, ${lng.toFixed(5)}`;\n}\n\n/** Render a nearby-search result as a numbered list (by distance, or travel time). */\nexport function renderNearby(result: NearbyResult): string {\n const { origin, results, total, routing } = result;\n const lines: string[] = [\n pc.bold(origin.displayName),\n pc.dim(\n `${coordString(origin.location.lat, origin.location.lng)} · ${total} found, showing ${results.length}`,\n ),\n ];\n if (routing) {\n const word = MODE_WORDS[routing.mode] ?? routing.mode;\n lines.push(\n pc.dim(\n routing.fellBack\n ? `ranked by ${word} time (straight-line estimate — routing engine unavailable)`\n : `ranked by ${word} time via ${routing.provider}`,\n ),\n );\n }\n lines.push('');\n\n if (results.length === 0) {\n lines.push(pc.dim('No amenities found within the search radius.'));\n return lines.join('\\n');\n }\n\n const rankWidth = String(results.length).length;\n for (const poi of results) {\n const rank = String(poi.rank).padStart(rankWidth);\n const label = CATEGORY_LABELS[poi.category];\n const name = poi.name ?? poi.kind ?? label;\n const distance = formatDistance(poi.distanceMeters);\n const metric =\n poi.travelSeconds !== undefined\n ? `${formatDuration(poi.travelSeconds)} · ${distance}`\n : distance;\n // With --explain, the reason (\"closest open cafe, 240 m\") replaces the meta.\n const meta = pc.dim(poi.rankingReason ?? `${label} · ${metric}`);\n const tags = `${accessibilityMark(poi.tags.wheelchair)}${poi.rankingReason ? '' : openMark(poi)}`;\n lines.push(`${pc.dim(`${rank}.`)} ${name} ${meta}${tags}`);\n }\n return lines.join('\\n');\n}\n\n/** A compact step-free indicator for the nearby list, shown only when tagged. */\nfunction accessibilityMark(wheelchair: string | undefined): string {\n if (wheelchair === 'yes') return ` ${pc.green('♿')}`;\n if (wheelchair === 'limited') return ` ${pc.yellow('♿ limited')}`;\n return '';\n}\n\nconst localHhmm = (iso: string): string => {\n const date = new Date(iso);\n return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;\n};\n\n/** An open/closed indicator, shown only when an `open` query was made. */\nfunction openMark(poi: RankedPoi): string {\n const state: OpenState | undefined = poi.openState;\n if (state === 'open') {\n const till = poi.nextChange ? ` ${pc.dim(`till ${localHhmm(poi.nextChange)}`)}` : '';\n return ` ${pc.green('open')}${till}`;\n }\n if (state === 'unknown') return ` ${pc.dim('hours?')}`;\n return '';\n}\n\n/** Render an isochrone-reachability result: what's within the time budget. */\nexport function renderReachable(result: ReachableResult): string {\n const { origin, results, withinMinutes, mode, isochrone, count } = result;\n const word = MODE_WORDS[mode] ?? mode;\n const basis = isochrone ? 'isochrone' : 'straight-line estimate';\n const lines: string[] = [\n pc.bold(origin.displayName),\n pc.dim(\n `${coordString(origin.location.lat, origin.location.lng)} · ` +\n `${count} reachable within ${withinMinutes} min ${word} (${basis})`,\n ),\n '',\n ];\n if (results.length === 0) {\n lines.push(pc.dim('Nothing reachable within that budget.'));\n return lines.join('\\n');\n }\n const rankWidth = String(results.length).length;\n for (const poi of results) {\n const rank = String(poi.rank).padStart(rankWidth);\n const label = CATEGORY_LABELS[poi.category];\n const name = poi.name ?? poi.kind ?? label;\n const distance = formatDistance(poi.distanceMeters);\n const metric =\n poi.travelSeconds !== undefined\n ? `${formatDuration(poi.travelSeconds)} · ${distance}`\n : distance;\n lines.push(\n `${pc.dim(`${rank}.`)} ${name} ${pc.dim(`${label} · ${metric}`)}${accessibilityMark(poi.tags.wheelchair)}`,\n );\n }\n return lines.join('\\n');\n}\n\n/** Render geocoding candidates with their addresses and coordinates. */\nexport function renderGeocode(places: Place[], ambiguous = false): string {\n if (places.length === 0) return 'No matches found.';\n\n const rankWidth = String(places.length).length;\n const lines: string[] = [];\n if (ambiguous) {\n lines.push(\n pc.yellow(`⚠ Ambiguous — ${places.length} distinct places match this name; pick one:`),\n '',\n );\n }\n places.forEach((place, index) => {\n const rank = String(index + 1).padStart(rankWidth);\n lines.push(`${pc.dim(`${rank}.`)} ${pc.bold(place.name)}`);\n lines.push(` ${pc.dim(place.displayName)}`);\n lines.push(` ${pc.dim(coordString(place.location.lat, place.location.lng))}`);\n });\n return lines.join('\\n');\n}\n\nfunction titleCase(value: string): string {\n return value.charAt(0).toUpperCase() + value.slice(1);\n}\n\n/** Render an amenity-gap report as a checklist with distances. */\nexport function renderGaps(report: GapReport): string {\n const { origin, gaps, missing, searchRadiusMeters, thresholdMeters } = report;\n const lines: string[] = [\n pc.bold(origin.displayName),\n pc.dim(\n `${coordString(origin.location.lat, origin.location.lng)} · searched ${formatDistance(searchRadiusMeters)}, gap if > ${formatDistance(thresholdMeters)}`,\n ),\n '',\n ];\n for (const gap of gaps) {\n const mark = gap.isGap ? pc.red('✗') : pc.green('✓');\n const detail =\n gap.nearestMeters === null\n ? pc.dim(`none within ${formatDistance(searchRadiusMeters)}`)\n : pc.dim(formatDistance(gap.nearestMeters));\n lines.push(`${mark} ${titleCase(gap.category)} ${detail}`);\n }\n lines.push('');\n lines.push(\n missing.length === 0\n ? pc.green('No gaps — all requested categories are nearby.')\n : pc.dim(`Missing (not found in OSM): ${missing.join(', ')}`),\n );\n return lines.join('\\n');\n}\n\n/** Render a walkability report: headline score, per-category breakdown, gaps. */\nexport function renderScore(report: WalkabilityReport): string {\n const { origin, score, confidence, breakdown, missing, decay } = report;\n const lines: string[] = [\n pc.bold(origin.displayName),\n pc.dim(\n `${coordString(origin.location.lat, origin.location.lng)} · ` +\n `walkability ${pc.bold(String(score))}/100 · confidence ${Math.round(confidence * 100)}%`,\n ),\n pc.dim(\n `full credit ≤ ${formatDistance(decay.idealMeters)}, none ≥ ${formatDistance(decay.maxMeters)}`,\n ),\n '',\n ];\n\n const nameWidth = Math.max(...breakdown.map((b) => titleCase(b.category).length));\n for (const entry of breakdown) {\n const mark =\n entry.subScore >= 0.67 ? pc.green('✓') : entry.subScore > 0 ? pc.yellow('~') : pc.red('✗');\n const name = titleCase(entry.category).padEnd(nameWidth);\n const detail =\n entry.nearestMeters === null\n ? pc.dim('none in range')\n : pc.dim(formatDistance(entry.nearestMeters));\n const pct = pc.dim(`${Math.round(entry.subScore * 100)}%`.padStart(4));\n lines.push(`${mark} ${name} ${pct} ${detail}`);\n }\n\n lines.push('');\n lines.push(\n missing.length === 0\n ? pc.green('All daily needs reachable within range.')\n : pc.dim(`Not found in OSM within range: ${missing.join(', ')}`),\n );\n if (confidence < 0.5) {\n lines.push(\n pc.yellow('Low confidence — OSM coverage here looks sparse; treat the score as a floor.'),\n );\n }\n return lines.join('\\n');\n}\n\n/** Render an errand plan: ordered stops with per-leg time, total, and any gaps. */\nexport function renderErrands(plan: ErrandPlan): string {\n const { origin, end, stops, totalSeconds, totalMeters, missing, mode } = plan;\n const word = MODE_WORDS[mode] ?? mode;\n const lines: string[] = [\n pc.bold(`Errand plan from ${origin.displayName}`),\n pc.dim(\n `${stops.length} stop${stops.length === 1 ? '' : 's'} · ` +\n `${formatDuration(totalSeconds)} ${word} · ${formatDistance(totalMeters)} (straight-line estimate)`,\n ),\n '',\n ];\n if (stops.length === 0) {\n lines.push(pc.dim('No reachable stops for the requested categories.'));\n }\n const stepWidth = String(stops.length).length;\n stops.forEach((stop, index) => {\n const step = String(index + 1).padStart(stepWidth);\n const name = stop.poi.name ?? stop.poi.kind ?? stop.category;\n const leg = pc.dim(`+${formatDuration(stop.legSeconds)} · ${formatDistance(stop.legMeters)}`);\n lines.push(`${pc.dim(`${step}.`)} ${pc.bold(titleCase(stop.category))}: ${name} ${leg}`);\n });\n if (end) lines.push(pc.dim(` ↳ end: ${end.displayName}`));\n if (missing.length > 0) {\n lines.push('');\n lines.push(pc.dim(`Not found nearby: ${missing.join(', ')}`));\n }\n return lines.join('\\n');\n}\n\n/** Render a location-comparison scorecard: ranked locations + per-dimension winners. */\nexport function renderComparison(report: ComparisonReport): string {\n const weightStr = report.weights.map((w) => `${titleCase(w.term)}×${w.weight}`).join(', ');\n const lines: string[] = [pc.bold('Location comparison'), pc.dim(`weights: ${weightStr}`), ''];\n\n const rankWidth = String(report.ranked.length).length;\n report.ranked.forEach((entry, position) => {\n const location = report.locations[entry.index]!;\n const marker = position === 0 ? pc.green('★') : ' ';\n const rank = String(position + 1).padStart(rankWidth);\n lines.push(`${marker} ${pc.dim(`${rank}.`)} ${pc.bold(entry.origin.displayName)}`);\n lines.push(\n ` ${pc.dim(`${entry.score}/100 · confidence ${Math.round(location.confidence * 100)}%`)}`,\n );\n });\n\n lines.push('');\n lines.push(pc.bold('Best per category:'));\n const nameWidth = Math.max(...report.dimensions.map((d) => titleCase(d.category).length));\n for (const dimension of report.dimensions) {\n const name = titleCase(dimension.category).padEnd(nameWidth);\n let detail: string;\n if (dimension.bestIndex === null) {\n detail = pc.dim('none found');\n } else {\n const location = report.locations[dimension.bestIndex]!;\n const entry = location.breakdown.find((b) => b.category === dimension.category);\n const distance =\n entry && entry.nearestMeters !== null\n ? pc.dim(` (${formatDistance(entry.nearestMeters)})`)\n : '';\n detail = `${location.origin.name}${distance}`;\n }\n lines.push(` ${name} ${detail}`);\n }\n return lines.join('\\n');\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@proximap/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Command-line interface for proximap — find and rank what's near any place.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"maps",
|
|
8
|
+
"geocoding",
|
|
9
|
+
"amenities",
|
|
10
|
+
"osm",
|
|
11
|
+
"proximity"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Ameya Borkar <ameyaborkar17@gmail.com>",
|
|
15
|
+
"homepage": "https://github.com/AmeyaBorkar/proximap#readme",
|
|
16
|
+
"bugs": "https://github.com/AmeyaBorkar/proximap/issues",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/AmeyaBorkar/proximap.git",
|
|
20
|
+
"directory": "packages/cli"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"proximap": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"prepublishOnly": "npm run build"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@proximap/core": "^1.0.0",
|
|
44
|
+
"commander": "^15.0.0",
|
|
45
|
+
"picocolors": "^1.1.1"
|
|
46
|
+
}
|
|
47
|
+
}
|