@tks/wayfinder 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -99,3 +99,27 @@ Structured hotel output for scripting:
99
99
  ```bash
100
100
  wayfinder hotels --where "Paris" --check-in 2026-04-10 --check-out 2026-04-12 --json | jq '.results[] | {name,nightlyPrice,rating}'
101
101
  ```
102
+
103
+ Search nearby restaurants from a location:
104
+
105
+ ```bash
106
+ wayfinder places --near "Shinjuku, Tokyo"
107
+ ```
108
+
109
+ Use a specific location name for better relevance:
110
+
111
+ ```bash
112
+ wayfinder places --near "Domino Park, Brooklyn, NY"
113
+ ```
114
+
115
+ Search nearby coffee spots:
116
+
117
+ ```bash
118
+ wayfinder places --near "Shinjuku, Tokyo" --type coffee --limit 5
119
+ ```
120
+
121
+ Structured places output for scripting:
122
+
123
+ ```bash
124
+ wayfinder places --near "Shinjuku, Tokyo" --type coffee --json | jq '.results[] | {name,rating,reviews,googleMapsUrl}'
125
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tks/wayfinder",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Travel search for your terminal and your AI agents",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { getConfigPath, readConfigApiKey, resolveApiKey, writeConfigApiKey } from "./config";
2
2
  import { CliError } from "./errors";
3
- import { renderFlightTable, renderHotelTable } from "./format";
3
+ import { renderFlightTable, renderHotelTable, renderPlaceTable } from "./format";
4
4
  import { parseCliArgs } from "./parse";
5
- import { searchFlightBookingOptions, searchFlights, searchHotels } from "./serpapi";
5
+ import { searchFlightBookingOptions, searchFlights, searchHotels, searchPlaces } from "./serpapi";
6
6
  import { ExitCode } from "./types";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
@@ -22,7 +22,7 @@ interface RunOptions {
22
22
  promptImpl?: (prompt: string) => Promise<string>;
23
23
  }
24
24
 
25
- const HELP_TEXT = `wayfinder v0.2.1 travel search
25
+ const HELP_TEXT = `wayfinder v0.3.0 travel search
26
26
 
27
27
  Usage:
28
28
  wayfinder setup [--reset]
@@ -30,6 +30,7 @@ Usage:
30
30
  wayfinder flights one-way --from SFO --to JFK --date 2026-03-21 [filters]
31
31
  wayfinder flights booking --from SFO --to JFK --date 2026-03-21 --token <BOOKING_TOKEN> [--token <BOOKING_TOKEN>] [--json]
32
32
  wayfinder hotels --where "New York, NY" --check-in 2026-03-21 --check-out 2026-03-23 [filters]
33
+ wayfinder places --near "Shinjuku, Tokyo" [--type restaurant|coffee] [--limit N] [--json]
33
34
 
34
35
  Setup:
35
36
  Runs interactive key setup and stores your SerpApi key in local config.
@@ -65,6 +66,14 @@ Hotels optional filters:
65
66
  --max-price <USD> Max nightly rate in USD
66
67
  --rating <3.5|4|4.5|5> Minimum guest rating
67
68
 
69
+ Places required:
70
+ --near <QUERY> Specific location query, example "Domino Park, Brooklyn, NY"
71
+ Broad names can return mixed-city results
72
+
73
+ Places optional filters:
74
+ --type <restaurant|coffee> Place type (default restaurant)
75
+ --limit <N> Maximum number of results (default 10)
76
+
68
77
  Output:
69
78
  --json Print structured JSON output`;
70
79
 
@@ -181,7 +190,7 @@ export async function runWayfinder(
181
190
  } else {
182
191
  output.stdout(renderFlightBookingText(flightLinks));
183
192
  }
184
- } else {
193
+ } else if (parsed.mode === "hotels") {
185
194
  const hotels = await searchHotels(parsed.query, apiKey, options.fetchImpl ?? fetch);
186
195
 
187
196
  if (hotels.length === 0) {
@@ -202,6 +211,27 @@ export async function runWayfinder(
202
211
  } else {
203
212
  output.stdout(renderHotelTable(hotels));
204
213
  }
214
+ } else {
215
+ const places = await searchPlaces(parsed.query, apiKey, options.fetchImpl ?? fetch);
216
+
217
+ if (places.length === 0) {
218
+ throw new CliError("No places found for the selected query", ExitCode.NoResults);
219
+ }
220
+
221
+ if (parsed.outputJson) {
222
+ output.stdout(
223
+ JSON.stringify(
224
+ {
225
+ query: parsed.query,
226
+ results: places,
227
+ },
228
+ null,
229
+ 2,
230
+ ),
231
+ );
232
+ } else {
233
+ output.stdout(renderPlaceTable(places));
234
+ }
205
235
  }
206
236
 
207
237
  return ExitCode.Success;
package/src/format.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { FlightOption, HotelOption } from "./types";
1
+ import { FlightOption, HotelOption, PlaceOption } from "./types";
2
2
 
3
3
  const currencyFormatter = new Intl.NumberFormat("en-US", {
4
4
  style: "currency",
@@ -142,6 +142,74 @@ export function renderHotelTable(options: HotelOption[]): string {
142
142
  return lines.join("\n");
143
143
  }
144
144
 
145
+ export function renderPlaceTable(options: PlaceOption[]): string {
146
+ const rows = options.map((option) => ({
147
+ name: option.name,
148
+ type: option.category,
149
+ rating: typeof option.rating === "number" ? option.rating.toFixed(1) : "n/a",
150
+ reviews: typeof option.reviews === "number" ? String(option.reviews) : "n/a",
151
+ distance: formatDistance(option.distanceMeters),
152
+ address: option.address ?? "n/a",
153
+ }));
154
+
155
+ const headers = {
156
+ name: "NAME",
157
+ type: "TYPE",
158
+ rating: "RATING",
159
+ reviews: "REVIEWS",
160
+ distance: "DISTANCE",
161
+ address: "ADDRESS",
162
+ };
163
+
164
+ const widths = {
165
+ name: maxWidth(rows, "name", headers.name),
166
+ type: maxWidth(rows, "type", headers.type),
167
+ rating: maxWidth(rows, "rating", headers.rating),
168
+ reviews: maxWidth(rows, "reviews", headers.reviews),
169
+ distance: maxWidth(rows, "distance", headers.distance),
170
+ address: maxWidth(rows, "address", headers.address),
171
+ };
172
+
173
+ const lines: string[] = [];
174
+
175
+ lines.push(
176
+ [
177
+ headers.name.padEnd(widths.name),
178
+ headers.type.padEnd(widths.type),
179
+ headers.rating.padEnd(widths.rating),
180
+ headers.reviews.padEnd(widths.reviews),
181
+ headers.distance.padEnd(widths.distance),
182
+ headers.address.padEnd(widths.address),
183
+ ].join(" "),
184
+ );
185
+
186
+ lines.push(
187
+ [
188
+ "-".repeat(widths.name),
189
+ "-".repeat(widths.type),
190
+ "-".repeat(widths.rating),
191
+ "-".repeat(widths.reviews),
192
+ "-".repeat(widths.distance),
193
+ "-".repeat(widths.address),
194
+ ].join(" "),
195
+ );
196
+
197
+ for (const row of rows) {
198
+ lines.push(
199
+ [
200
+ row.name.padEnd(widths.name),
201
+ row.type.padEnd(widths.type),
202
+ row.rating.padEnd(widths.rating),
203
+ row.reviews.padEnd(widths.reviews),
204
+ row.distance.padEnd(widths.distance),
205
+ row.address.padEnd(widths.address),
206
+ ].join(" "),
207
+ );
208
+ }
209
+
210
+ return lines.join("\n");
211
+ }
212
+
145
213
  function maxWidth(rows: Array<Record<string, string>>, key: string, header: string): number {
146
214
  return rows.reduce((width, row) => Math.max(width, row[key].length), header.length);
147
215
  }
@@ -164,3 +232,16 @@ function formatDuration(totalMinutes: number): string {
164
232
 
165
233
  return `${hours}h ${minutes}m`;
166
234
  }
235
+
236
+ function formatDistance(distanceMeters?: number): string {
237
+ if (!Number.isFinite(distanceMeters)) {
238
+ return "n/a";
239
+ }
240
+
241
+ if ((distanceMeters as number) < 1000) {
242
+ return `${Math.round(distanceMeters as number)}m`;
243
+ }
244
+
245
+ const km = (distanceMeters as number) / 1000;
246
+ return `${km.toFixed(1)}km`;
247
+ }
package/src/parse.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  import { CliError } from "./errors";
2
- import { ExitCode, FlightBookingQuery, FlightQuery, HotelQuery, ParsedArgs } from "./types";
2
+ import {
3
+ ExitCode,
4
+ FlightBookingQuery,
5
+ FlightQuery,
6
+ HotelQuery,
7
+ ParsedArgs,
8
+ PlaceQuery,
9
+ PlaceType,
10
+ } from "./types";
3
11
 
4
12
  interface FlightRawOptions {
5
13
  from?: string;
@@ -35,7 +43,15 @@ interface FlightBookingRawOptions {
35
43
  help: boolean;
36
44
  }
37
45
 
38
- type SearchMode = "flights" | "hotels" | "flight-booking" | "setup";
46
+ interface PlaceRawOptions {
47
+ near?: string;
48
+ type?: string;
49
+ limit?: string;
50
+ outputJson: boolean;
51
+ help: boolean;
52
+ }
53
+
54
+ type SearchMode = "flights" | "hotels" | "places" | "flight-booking" | "setup";
39
55
 
40
56
  const HELP_FLAGS = new Set(["-h", "--help"]);
41
57
 
@@ -57,6 +73,10 @@ export function parseCliArgs(argv: string[]): ParsedArgs {
57
73
  return parseFlightBookingArgs(args);
58
74
  }
59
75
 
76
+ if (mode === "places") {
77
+ return parsePlacesArgs(args);
78
+ }
79
+
60
80
  if (mode === "setup") {
61
81
  return parseSetupArgs(args);
62
82
  }
@@ -281,6 +301,66 @@ function parseFlightBookingArgs(args: string[]): ParsedArgs {
281
301
  };
282
302
  }
283
303
 
304
+ function parsePlacesArgs(args: string[]): ParsedArgs {
305
+ const raw: PlaceRawOptions = {
306
+ outputJson: false,
307
+ help: false,
308
+ };
309
+
310
+ for (let i = 0; i < args.length; i += 1) {
311
+ const token = args[i];
312
+
313
+ if (HELP_FLAGS.has(token)) {
314
+ raw.help = true;
315
+ continue;
316
+ }
317
+
318
+ if (token === "--json") {
319
+ raw.outputJson = true;
320
+ continue;
321
+ }
322
+
323
+ if (!token.startsWith("--")) {
324
+ throw new CliError(`Unexpected argument: ${token}`, ExitCode.InvalidInput);
325
+ }
326
+
327
+ const value = args[i + 1];
328
+ if (!value || value.startsWith("--")) {
329
+ throw new CliError(`Missing value for ${token}`, ExitCode.InvalidInput);
330
+ }
331
+
332
+ switch (token) {
333
+ case "--near":
334
+ raw.near = value;
335
+ break;
336
+ case "--type":
337
+ raw.type = value;
338
+ break;
339
+ case "--limit":
340
+ raw.limit = value;
341
+ break;
342
+ default:
343
+ throw new CliError(`Unknown flag: ${token}`, ExitCode.InvalidInput);
344
+ }
345
+
346
+ i += 1;
347
+ }
348
+
349
+ if (raw.help) {
350
+ return {
351
+ help: true,
352
+ outputJson: raw.outputJson,
353
+ };
354
+ }
355
+
356
+ return {
357
+ help: false,
358
+ mode: "places",
359
+ outputJson: raw.outputJson,
360
+ query: buildPlaceQuery(raw),
361
+ };
362
+ }
363
+
284
364
  function parseSetupArgs(args: string[]): ParsedArgs {
285
365
  const outputJson = args.includes("--json");
286
366
  const reset = args.includes("--reset");
@@ -409,6 +489,22 @@ function buildFlightBookingQuery(raw: FlightBookingRawOptions): FlightBookingQue
409
489
  };
410
490
  }
411
491
 
492
+ function buildPlaceQuery(raw: PlaceRawOptions): PlaceQuery {
493
+ if (!raw.near) {
494
+ throw new CliError("Missing required flag: --near", ExitCode.InvalidInput);
495
+ }
496
+
497
+ const near = normalizeLocation(raw.near);
498
+ const type = raw.type ? normalizePlaceType(raw.type) : "restaurant";
499
+ const limit = raw.limit ? normalizeLimit(raw.limit, "--limit") : 10;
500
+
501
+ return {
502
+ near,
503
+ type,
504
+ limit,
505
+ };
506
+ }
507
+
412
508
  function stripSubcommands(argv: string[]): { mode: SearchMode; args: string[] } {
413
509
  const args = [...argv];
414
510
  if (args[0] === "hotels") {
@@ -435,8 +531,13 @@ function stripSubcommands(argv: string[]): { mode: SearchMode; args: string[] }
435
531
  return { mode: "setup", args };
436
532
  }
437
533
 
534
+ if (args[0] === "places") {
535
+ args.shift();
536
+ return { mode: "places", args };
537
+ }
538
+
438
539
  throw new CliError(
439
- "Missing subcommand: use `setup`, `flights`, or `hotels`",
540
+ "Missing subcommand: use `setup`, `flights`, `hotels`, or `places`",
440
541
  ExitCode.InvalidInput,
441
542
  );
442
543
  }
@@ -521,6 +622,15 @@ function normalizeMaxPrice(value: string): number {
521
622
  return numeric;
522
623
  }
523
624
 
625
+ function normalizeLimit(value: string, flagName: string): number {
626
+ const numeric = Number.parseInt(value, 10);
627
+ if (!Number.isInteger(numeric) || numeric <= 0) {
628
+ throw new CliError(`${flagName} must be a positive integer`, ExitCode.InvalidInput);
629
+ }
630
+
631
+ return numeric;
632
+ }
633
+
524
634
  function normalizeAdults(value: string): number {
525
635
  const numeric = Number.parseInt(value, 10);
526
636
  if (!Number.isInteger(numeric) || numeric <= 0) {
@@ -539,6 +649,15 @@ function normalizeMinRating(value: string): 3.5 | 4 | 4.5 | 5 {
539
649
  return numeric;
540
650
  }
541
651
 
652
+ function normalizePlaceType(value: string): PlaceType {
653
+ const normalized = value.trim().toLowerCase();
654
+ if (normalized !== "restaurant" && normalized !== "coffee") {
655
+ throw new CliError("--type must be one of: restaurant, coffee", ExitCode.InvalidInput);
656
+ }
657
+
658
+ return normalized;
659
+ }
660
+
542
661
  function normalizeTime(value: string, flagName: string): number {
543
662
  const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value);
544
663
  if (!match) {
package/src/serpapi.ts CHANGED
@@ -8,6 +8,9 @@ import {
8
8
  FlightSearchResult,
9
9
  HotelOption,
10
10
  HotelQuery,
11
+ PlaceOption,
12
+ PlaceQuery,
13
+ PlaceType,
11
14
  } from "./types";
12
15
 
13
16
  interface SerpApiAirport {
@@ -80,6 +83,23 @@ interface SerpApiHotelsResponse {
80
83
  properties?: SerpApiHotelProperty[];
81
84
  }
82
85
 
86
+ interface SerpApiLocalResult {
87
+ title?: string;
88
+ rating?: number;
89
+ reviews?: number;
90
+ address?: string;
91
+ distance?: string;
92
+ open_state?: string;
93
+ type?: string;
94
+ place_id_search?: string;
95
+ data_id?: string;
96
+ }
97
+
98
+ interface SerpApiPlacesResponse {
99
+ error?: string;
100
+ local_results?: SerpApiLocalResult[];
101
+ }
102
+
83
103
  export async function searchFlights(
84
104
  query: FlightQuery,
85
105
  apiKey: string,
@@ -158,6 +178,25 @@ export async function searchHotels(
158
178
  return hotels;
159
179
  }
160
180
 
181
+ export async function searchPlaces(
182
+ query: PlaceQuery,
183
+ apiKey: string,
184
+ fetchImpl: typeof fetch = fetch,
185
+ ): Promise<PlaceOption[]> {
186
+ const payload = await fetchSerpApiJson<SerpApiPlacesResponse>(
187
+ buildPlaceRequestUrl(query, apiKey),
188
+ fetchImpl,
189
+ );
190
+
191
+ if (typeof payload.error === "string" && payload.error.trim() !== "") {
192
+ throw new CliError(`SerpApi error: ${payload.error}`, ExitCode.ApiFailure);
193
+ }
194
+
195
+ const places = shapePlaceSerpApiResponse(payload, query.type);
196
+ places.sort((a, b) => b.score - a.score);
197
+ return places.slice(0, query.limit);
198
+ }
199
+
161
200
  export function shapeSerpApiResponse(payload: SerpApiFlightsResponse): FlightOption[] {
162
201
  const merged = [...(payload.best_flights ?? []), ...(payload.other_flights ?? [])];
163
202
 
@@ -172,6 +211,15 @@ export function shapeHotelSerpApiResponse(payload: SerpApiHotelsResponse): Hotel
172
211
  .filter((option): option is HotelOption => option !== null);
173
212
  }
174
213
 
214
+ export function shapePlaceSerpApiResponse(
215
+ payload: SerpApiPlacesResponse,
216
+ category: PlaceType,
217
+ ): PlaceOption[] {
218
+ return (payload.local_results ?? [])
219
+ .map((result) => shapeLocalResult(result, category))
220
+ .filter((option): option is PlaceOption => option !== null);
221
+ }
222
+
175
223
  export function filterByDepartureWindow(
176
224
  options: FlightOption[],
177
225
  minMinutes: number,
@@ -336,6 +384,17 @@ function buildHotelRequestUrl(query: HotelQuery, apiKey: string): string {
336
384
  return url.toString();
337
385
  }
338
386
 
387
+ function buildPlaceRequestUrl(query: PlaceQuery, apiKey: string): string {
388
+ const url = new URL("https://serpapi.com/search.json");
389
+ const searchTerm = query.type === "coffee" ? "coffee" : "restaurants";
390
+
391
+ url.searchParams.set("engine", "google_maps");
392
+ url.searchParams.set("type", "search");
393
+ url.searchParams.set("q", `${searchTerm} near ${query.near}`);
394
+ url.searchParams.set("api_key", apiKey);
395
+ return url.toString();
396
+ }
397
+
339
398
  function buildFlightBookingOptionsRequestUrl(
340
399
  query: FlightBookingQuery,
341
400
  token: string,
@@ -383,6 +442,101 @@ function toSerpApiRating(rating: 3.5 | 4 | 4.5 | 5): string {
383
442
  return "10";
384
443
  }
385
444
 
445
+ function shapeLocalResult(result: SerpApiLocalResult, category: PlaceType): PlaceOption | null {
446
+ if (typeof result.title !== "string" || result.title.trim() === "") {
447
+ return null;
448
+ }
449
+
450
+ const rating = Number.isFinite(result.rating) ? (result.rating as number) : undefined;
451
+ const reviews = Number.isFinite(result.reviews) ? (result.reviews as number) : undefined;
452
+ const distanceMeters = parseDistanceMeters(result.distance);
453
+ const link = buildGoogleMapsPlaceLink(result);
454
+ const googleMapsUrl = buildDirectGoogleMapsUrl(result);
455
+
456
+ return {
457
+ name: result.title.trim(),
458
+ category,
459
+ rating,
460
+ reviews,
461
+ address: typeof result.address === "string" ? result.address : undefined,
462
+ distanceMeters,
463
+ openState: typeof result.open_state === "string" ? result.open_state : undefined,
464
+ link,
465
+ googleMapsUrl,
466
+ score: computePlaceScore(rating, reviews, distanceMeters),
467
+ };
468
+ }
469
+
470
+ function computePlaceScore(
471
+ rating: number | undefined,
472
+ reviews: number | undefined,
473
+ distanceMeters: number | undefined,
474
+ ): number {
475
+ const ratingComponent = typeof rating === "number" ? rating / 5 : 0;
476
+ const reviewComponent =
477
+ typeof reviews === "number" && reviews > 0 ? Math.log10(reviews + 1) / 4 : 0;
478
+ const distanceComponent =
479
+ typeof distanceMeters === "number" && distanceMeters >= 0
480
+ ? Math.max(0, 1 - distanceMeters / 10_000)
481
+ : 0;
482
+
483
+ return ratingComponent * 0.6 + reviewComponent * 0.25 + distanceComponent * 0.15;
484
+ }
485
+
486
+ function parseDistanceMeters(value?: string): number | undefined {
487
+ if (typeof value !== "string" || value.trim() === "") {
488
+ return undefined;
489
+ }
490
+
491
+ const normalized = value.trim().toLowerCase().replace(/,/g, "");
492
+ const match = /^(\d+(?:\.\d+)?)\s*(m|meter|meters|km|mi)$/.exec(normalized);
493
+ if (!match) {
494
+ return undefined;
495
+ }
496
+
497
+ const amount = Number(match[1]);
498
+ const unit = match[2];
499
+ if (!Number.isFinite(amount)) {
500
+ return undefined;
501
+ }
502
+
503
+ if (unit === "m" || unit === "meter" || unit === "meters") {
504
+ return amount;
505
+ }
506
+
507
+ if (unit === "km") {
508
+ return amount * 1000;
509
+ }
510
+
511
+ return amount * 1609.34;
512
+ }
513
+
514
+ function buildGoogleMapsPlaceLink(result: SerpApiLocalResult): string | undefined {
515
+ if (typeof result.place_id_search === "string" && result.place_id_search.trim() !== "") {
516
+ return result.place_id_search;
517
+ }
518
+
519
+ return undefined;
520
+ }
521
+
522
+ function buildDirectGoogleMapsUrl(result: SerpApiLocalResult): string | undefined {
523
+ if (typeof result.place_id_search !== "string" || result.place_id_search.trim() === "") {
524
+ return undefined;
525
+ }
526
+
527
+ try {
528
+ const parsed = new URL(result.place_id_search);
529
+ const placeId = parsed.searchParams.get("place_id");
530
+ if (!placeId || placeId.trim() === "") {
531
+ return undefined;
532
+ }
533
+
534
+ return `https://www.google.com/maps/place/?q=place_id:${encodeURIComponent(placeId)}`;
535
+ } catch {
536
+ return undefined;
537
+ }
538
+ }
539
+
386
540
  async function fetchSerpApiJson<T>(
387
541
  requestUrl: string,
388
542
  fetchImpl: typeof fetch,
package/src/types.ts CHANGED
@@ -30,6 +30,14 @@ export interface HotelQuery {
30
30
  minRating?: 3.5 | 4 | 4.5 | 5;
31
31
  }
32
32
 
33
+ export type PlaceType = "restaurant" | "coffee";
34
+
35
+ export interface PlaceQuery {
36
+ near: string;
37
+ type: PlaceType;
38
+ limit: number;
39
+ }
40
+
33
41
  export interface ParsedArgsHelp {
34
42
  help: true;
35
43
  outputJson: boolean;
@@ -70,10 +78,18 @@ export interface ParsedArgsSetup {
70
78
  reset: boolean;
71
79
  }
72
80
 
81
+ export interface ParsedArgsPlaces {
82
+ help: false;
83
+ mode: "places";
84
+ outputJson: boolean;
85
+ query: PlaceQuery;
86
+ }
87
+
73
88
  export type ParsedArgs =
74
89
  | ParsedArgsHelp
75
90
  | ParsedArgsFlights
76
91
  | ParsedArgsHotels
92
+ | ParsedArgsPlaces
77
93
  | ParsedArgsFlightBooking
78
94
  | ParsedArgsSetup;
79
95
 
@@ -97,6 +113,19 @@ export interface HotelOption {
97
113
  link?: string;
98
114
  }
99
115
 
116
+ export interface PlaceOption {
117
+ name: string;
118
+ category: PlaceType;
119
+ rating?: number;
120
+ reviews?: number;
121
+ address?: string;
122
+ distanceMeters?: number;
123
+ openState?: string;
124
+ link?: string;
125
+ googleMapsUrl?: string;
126
+ score: number;
127
+ }
128
+
100
129
  export interface FlightSearchResult {
101
130
  options: FlightOption[];
102
131
  googleFlightsUrl?: string;