mta-js 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/src/http.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { FeedError } from "./errors";
2
+
3
+ export async function fetchArrayBuffer(fetchImpl: typeof fetch, url: string) {
4
+ const response = await fetchImpl(url);
5
+ if (!response.ok) {
6
+ throw new FeedError(`MTA feed request failed: ${response.status} ${response.statusText}`, response);
7
+ }
8
+ return response.arrayBuffer();
9
+ }
10
+
11
+ export async function fetchJson(fetchImpl: typeof fetch, url: string) {
12
+ const response = await fetchImpl(url);
13
+ if (!response.ok) {
14
+ throw new FeedError(`MTA API request failed: ${response.status} ${response.statusText}`, response);
15
+ }
16
+ return response.json() as Promise<unknown>;
17
+ }
18
+
19
+ export function urlWithParams(base: string, params: Record<string, string | number | undefined>) {
20
+ const url = new URL(base);
21
+ for (const [key, value] of Object.entries(params)) {
22
+ if (value !== undefined && value !== "") {
23
+ url.searchParams.set(key, String(value));
24
+ }
25
+ }
26
+ return url.toString();
27
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,51 @@
1
+ export const gtfsSchemaStatements = [
2
+ `create table if not exists stops (
3
+ id text primary key,
4
+ name text not null,
5
+ lat real,
6
+ lon real,
7
+ parent_station text,
8
+ location_type integer,
9
+ mode text
10
+ )`,
11
+ `create table if not exists routes (
12
+ id text primary key,
13
+ short_name text,
14
+ long_name text,
15
+ type integer,
16
+ color text,
17
+ text_color text
18
+ )`,
19
+ `create table if not exists trips (
20
+ id text primary key,
21
+ route_id text not null,
22
+ service_id text,
23
+ headsign text,
24
+ direction_id integer
25
+ )`,
26
+ `create table if not exists stop_times (
27
+ trip_id text not null,
28
+ arrival_time text,
29
+ departure_time text,
30
+ stop_id text not null,
31
+ stop_sequence integer,
32
+ primary key (trip_id, stop_sequence, stop_id)
33
+ )`,
34
+ `create table if not exists gtfs_imports (
35
+ mode text primary key,
36
+ imported_at text not null,
37
+ source_url text,
38
+ stop_count integer not null default 0,
39
+ route_count integer not null default 0,
40
+ trip_count integer not null default 0,
41
+ stop_time_count integer not null default 0
42
+ )`,
43
+ "create index if not exists stops_parent_station_idx on stops(parent_station)",
44
+ "create index if not exists stops_lat_lon_idx on stops(lat, lon)",
45
+ "create index if not exists routes_short_name_idx on routes(short_name)",
46
+ "create index if not exists stop_times_stop_id_idx on stop_times(stop_id)",
47
+ ] as const;
48
+
49
+ export function gtfsSchemaSql() {
50
+ return `${gtfsSchemaStatements.join(";\n")};`;
51
+ }
@@ -0,0 +1,400 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { unzipSync } from "fflate";
3
+ import { parse } from "csv-parse/sync";
4
+ import { gtfsSchemaSql } from "./schema";
5
+ import type {
6
+ GtfsRouteInput,
7
+ GtfsImportSummary,
8
+ GtfsStopInput,
9
+ GtfsStopTimeInput,
10
+ GtfsTripInput,
11
+ Route,
12
+ StaticGtfsSeed,
13
+ DatabaseStatus,
14
+ StaticDataStatus,
15
+ Stop,
16
+ StopsNearQuery,
17
+ TransitMode,
18
+ } from "./types";
19
+
20
+ type StopRow = {
21
+ id: string;
22
+ name: string;
23
+ lat: number | null;
24
+ lon: number | null;
25
+ parent_station: string | null;
26
+ location_type: number | null;
27
+ mode: TransitMode | null;
28
+ };
29
+
30
+ type RouteRow = {
31
+ id: string;
32
+ short_name: string | null;
33
+ long_name: string | null;
34
+ type: number | null;
35
+ color: string | null;
36
+ text_color: string | null;
37
+ };
38
+
39
+ type TripRow = {
40
+ id: string;
41
+ route_id: string;
42
+ service_id: string | null;
43
+ headsign: string | null;
44
+ direction_id: number | null;
45
+ };
46
+
47
+ export class GTFSCache {
48
+ readonly db: Database;
49
+
50
+ constructor(path = ":memory:", options: { createSchema?: boolean } = {}) {
51
+ this.db = new Database(path, { create: true, strict: true });
52
+ this.db.run("PRAGMA journal_mode = WAL;");
53
+ if (options.createSchema ?? true) {
54
+ this.createSchema();
55
+ }
56
+ }
57
+
58
+ close() {
59
+ this.db.close(false);
60
+ }
61
+
62
+ pushSchema() {
63
+ this.createSchema();
64
+ }
65
+
66
+ importSeed(seed: StaticGtfsSeed, mode?: TransitMode) {
67
+ this.importRows({
68
+ stops: seed.stops ?? [],
69
+ routes: seed.routes ?? [],
70
+ trips: seed.trips ?? [],
71
+ stopTimes: seed.stopTimes ?? [],
72
+ mode,
73
+ });
74
+ if (mode) this.markImported(mode, undefined, seed);
75
+ }
76
+
77
+ async importZip(zipBytes: ArrayBuffer | Uint8Array, mode?: TransitMode) {
78
+ const seed = parseGtfsZip(zipBytes);
79
+ this.importRows({ ...seed, mode });
80
+ if (mode) this.markImported(mode, undefined, seed);
81
+ }
82
+
83
+ async importZipFromUrl(url: string, mode?: TransitMode, fetchImpl: typeof fetch = fetch) {
84
+ const response = await fetchImpl(url);
85
+ if (!response.ok) {
86
+ throw new Error(`Failed to fetch GTFS zip from ${url}: ${response.status}`);
87
+ }
88
+ await this.importZip(await response.arrayBuffer(), mode);
89
+ if (mode) {
90
+ this.db
91
+ .query("update gtfs_imports set source_url = ?1 where mode = ?2")
92
+ .run(url, mode);
93
+ }
94
+ }
95
+
96
+ hasStaticData(mode: TransitMode) {
97
+ const row = this.db
98
+ .query<{ count: number }, [TransitMode]>("select count(*) as count from gtfs_imports where mode = ?1")
99
+ .get(mode);
100
+ return Boolean(row?.count);
101
+ }
102
+
103
+ status(): DatabaseStatus {
104
+ return {
105
+ subway: this.statusForMode("subway"),
106
+ bus: this.statusForMode("bus"),
107
+ lirr: this.statusForMode("lirr"),
108
+ "metro-north": this.statusForMode("metro-north"),
109
+ };
110
+ }
111
+
112
+ importSummary(mode: TransitMode): GtfsImportSummary | undefined {
113
+ const row = this.db
114
+ .query<
115
+ {
116
+ mode: TransitMode;
117
+ imported_at: string;
118
+ source_url: string | null;
119
+ stop_count: number;
120
+ route_count: number;
121
+ trip_count: number;
122
+ stop_time_count: number;
123
+ },
124
+ [TransitMode]
125
+ >("select * from gtfs_imports where mode = ?1")
126
+ .get(mode);
127
+ if (!row) return undefined;
128
+ return {
129
+ mode: row.mode,
130
+ importedAt: row.imported_at,
131
+ sourceUrl: row.source_url ?? undefined,
132
+ stopCount: row.stop_count,
133
+ routeCount: row.route_count,
134
+ tripCount: row.trip_count,
135
+ stopTimeCount: row.stop_time_count,
136
+ };
137
+ }
138
+
139
+ private statusForMode(mode: TransitMode): StaticDataStatus {
140
+ const summary = this.importSummary(mode);
141
+ return {
142
+ mode,
143
+ ready: Boolean(summary),
144
+ importedAt: summary?.importedAt,
145
+ sourceUrl: summary?.sourceUrl,
146
+ stopCount: summary?.stopCount ?? 0,
147
+ routeCount: summary?.routeCount ?? 0,
148
+ tripCount: summary?.tripCount ?? 0,
149
+ stopTimeCount: summary?.stopTimeCount ?? 0,
150
+ };
151
+ }
152
+
153
+ getStop(id: string): Stop | undefined {
154
+ const row = this.db.query<StopRow, [string]>("select * from stops where id = ?1").get(id);
155
+ return row ? stopFromRow(row) : undefined;
156
+ }
157
+
158
+ getStopOrParent(id: string): Stop | undefined {
159
+ const direct = this.getStop(id);
160
+ if (direct?.parentStation) return this.getStop(direct.parentStation) ?? direct;
161
+ if (direct) return direct;
162
+ return this.getStop(stripDirectionSuffix(id));
163
+ }
164
+
165
+ getRoute(idOrShortName: string): Route | undefined {
166
+ const normalized = idOrShortName.toUpperCase();
167
+ const row = this.db
168
+ .query<RouteRow, [string, string]>(
169
+ "select * from routes where upper(id) = ?1 or upper(short_name) = ?2 limit 1",
170
+ )
171
+ .get(normalized, normalized);
172
+ return row ? routeFromRow(row) : undefined;
173
+ }
174
+
175
+ getTrip(id: string): TripRow | undefined {
176
+ return this.db.query<TripRow, [string]>("select * from trips where id = ?1").get(id) ?? undefined;
177
+ }
178
+
179
+ getStopIdsForQuery(stopId: string) {
180
+ const ids = new Set([stopId]);
181
+ const parent = stripDirectionSuffix(stopId);
182
+ ids.add(parent);
183
+ ids.add(`${parent}N`);
184
+ ids.add(`${parent}S`);
185
+
186
+ for (const row of this.db
187
+ .query<{ id: string }, [string]>("select id from stops where parent_station = ?1")
188
+ .all(parent)) {
189
+ ids.add(row.id);
190
+ }
191
+
192
+ return ids;
193
+ }
194
+
195
+ stopsNear(query: StopsNearQuery): Stop[] {
196
+ const radiusMeters = query.radiusMeters ?? 500;
197
+ const limit = query.limit ?? 20;
198
+ const latSpan = radiusMeters / 111_320;
199
+ const lonSpan = radiusMeters / (111_320 * Math.cos((query.lat * Math.PI) / 180));
200
+ const modes = query.modes?.length ? query.modes : undefined;
201
+
202
+ const rows = this.db
203
+ .query<StopRow, [number, number, number, number]>(
204
+ `select * from stops
205
+ where lat between ?1 and ?2
206
+ and lon between ?3 and ?4
207
+ and lat is not null
208
+ and lon is not null`,
209
+ )
210
+ .all(query.lat - latSpan, query.lat + latSpan, query.lon - lonSpan, query.lon + lonSpan);
211
+
212
+ return rows
213
+ .map((row) => ({ stop: stopFromRow(row), distance: distanceMeters(query.lat, query.lon, row.lat!, row.lon!) }))
214
+ .filter((row) => row.distance <= radiusMeters)
215
+ .filter((row) => !modes || !row.stop.mode || modes.includes(row.stop.mode))
216
+ .sort((a, b) => a.distance - b.distance)
217
+ .slice(0, limit)
218
+ .map((row) => row.stop);
219
+ }
220
+
221
+ private createSchema() {
222
+ this.db.run(gtfsSchemaSql());
223
+ }
224
+
225
+ private importRows(input: {
226
+ stops: GtfsStopInput[];
227
+ routes: GtfsRouteInput[];
228
+ trips: GtfsTripInput[];
229
+ stopTimes: GtfsStopTimeInput[];
230
+ mode?: TransitMode;
231
+ }) {
232
+ const insertStop = this.db.query(`
233
+ insert or replace into stops
234
+ (id, name, lat, lon, parent_station, location_type, mode)
235
+ values ($id, $name, $lat, $lon, $parentStation, $locationType, $mode)
236
+ `);
237
+ const insertRoute = this.db.query(`
238
+ insert or replace into routes
239
+ (id, short_name, long_name, type, color, text_color)
240
+ values ($id, $shortName, $longName, $type, $color, $textColor)
241
+ `);
242
+ const insertTrip = this.db.query(`
243
+ insert or replace into trips
244
+ (id, route_id, service_id, headsign, direction_id)
245
+ values ($id, $routeId, $serviceId, $headsign, $directionId)
246
+ `);
247
+ const insertStopTime = this.db.query(`
248
+ insert or replace into stop_times
249
+ (trip_id, arrival_time, departure_time, stop_id, stop_sequence)
250
+ values ($tripId, $arrivalTime, $departureTime, $stopId, $stopSequence)
251
+ `);
252
+
253
+ const transaction = this.db.transaction(() => {
254
+ for (const stop of input.stops) {
255
+ insertStop.run({
256
+ id: stop.stop_id,
257
+ name: stop.stop_name,
258
+ lat: numberOrNull(stop.stop_lat),
259
+ lon: numberOrNull(stop.stop_lon),
260
+ parentStation: stop.parent_station || null,
261
+ locationType: numberOrNull(stop.location_type),
262
+ mode: input.mode ?? inferModeFromRouteType(undefined),
263
+ });
264
+ }
265
+
266
+ for (const route of input.routes) {
267
+ insertRoute.run({
268
+ id: route.route_id,
269
+ shortName: route.route_short_name || route.route_id,
270
+ longName: route.route_long_name || null,
271
+ type: numberOrNull(route.route_type),
272
+ color: normalizeColor(route.route_color),
273
+ textColor: normalizeColor(route.route_text_color),
274
+ });
275
+ }
276
+
277
+ for (const trip of input.trips) {
278
+ insertTrip.run({
279
+ id: trip.trip_id,
280
+ routeId: trip.route_id,
281
+ serviceId: trip.service_id || null,
282
+ headsign: trip.trip_headsign || null,
283
+ directionId: numberOrNull(trip.direction_id),
284
+ });
285
+ }
286
+
287
+ for (const stopTime of input.stopTimes) {
288
+ insertStopTime.run({
289
+ tripId: stopTime.trip_id,
290
+ arrivalTime: stopTime.arrival_time || null,
291
+ departureTime: stopTime.departure_time || null,
292
+ stopId: stopTime.stop_id,
293
+ stopSequence: numberOrNull(stopTime.stop_sequence) ?? 0,
294
+ });
295
+ }
296
+ });
297
+
298
+ transaction();
299
+ }
300
+
301
+ private markImported(mode: TransitMode, sourceUrl: string | undefined, seed: StaticGtfsSeed) {
302
+ this.db
303
+ .query(`
304
+ insert or replace into gtfs_imports
305
+ (mode, imported_at, source_url, stop_count, route_count, trip_count, stop_time_count)
306
+ values ($mode, $importedAt, $sourceUrl, $stopCount, $routeCount, $tripCount, $stopTimeCount)
307
+ `)
308
+ .run({
309
+ mode,
310
+ importedAt: new Date().toISOString(),
311
+ sourceUrl: sourceUrl ?? null,
312
+ stopCount: seed.stops?.length ?? 0,
313
+ routeCount: seed.routes?.length ?? 0,
314
+ tripCount: seed.trips?.length ?? 0,
315
+ stopTimeCount: seed.stopTimes?.length ?? 0,
316
+ });
317
+ }
318
+ }
319
+
320
+ export function stripDirectionSuffix(stopId: string) {
321
+ return stopId.replace(/[NS]$/, "");
322
+ }
323
+
324
+ export function directionFromStopId(stopId: string) {
325
+ if (stopId.endsWith("N")) return "north";
326
+ if (stopId.endsWith("S")) return "south";
327
+ return "unknown";
328
+ }
329
+
330
+ export function parseGtfsZip(zipBytes: ArrayBuffer | Uint8Array): Required<StaticGtfsSeed> {
331
+ const files = unzipSync(new Uint8Array(zipBytes));
332
+ const text = (name: string) => {
333
+ const bytes = files[name];
334
+ if (!bytes) return [];
335
+ return parse(new TextDecoder().decode(bytes), {
336
+ columns: true,
337
+ bom: true,
338
+ skip_empty_lines: true,
339
+ }) as Record<string, string>[];
340
+ };
341
+
342
+ return {
343
+ stops: text("stops.txt") as unknown as GtfsStopInput[],
344
+ routes: text("routes.txt") as unknown as GtfsRouteInput[],
345
+ trips: text("trips.txt") as unknown as GtfsTripInput[],
346
+ stopTimes: text("stop_times.txt") as unknown as GtfsStopTimeInput[],
347
+ };
348
+ }
349
+
350
+ function stopFromRow(row: StopRow): Stop {
351
+ return {
352
+ id: row.id,
353
+ name: row.name,
354
+ lat: row.lat ?? undefined,
355
+ lon: row.lon ?? undefined,
356
+ parentStation: row.parent_station ?? undefined,
357
+ mode: row.mode ?? undefined,
358
+ };
359
+ }
360
+
361
+ function routeFromRow(row: RouteRow): Route {
362
+ return {
363
+ id: row.id,
364
+ shortName: row.short_name ?? undefined,
365
+ longName: row.long_name ?? undefined,
366
+ type: row.type ?? undefined,
367
+ color: row.color ?? undefined,
368
+ textColor: row.text_color ?? undefined,
369
+ };
370
+ }
371
+
372
+ function numberOrNull(value: unknown) {
373
+ if (value === undefined || value === null || value === "") return null;
374
+ const number = Number(value);
375
+ return Number.isFinite(number) ? number : null;
376
+ }
377
+
378
+ function normalizeColor(value: string | undefined) {
379
+ if (!value) return null;
380
+ return value.startsWith("#") ? value : `#${value}`;
381
+ }
382
+
383
+ function inferModeFromRouteType(type?: number): TransitMode | null {
384
+ if (type === 1) return "subway";
385
+ if (type === 2) return "lirr";
386
+ if (type === 3) return "bus";
387
+ return null;
388
+ }
389
+
390
+ function distanceMeters(lat1: number, lon1: number, lat2: number, lon2: number) {
391
+ const radius = 6_371_000;
392
+ const phi1 = (lat1 * Math.PI) / 180;
393
+ const phi2 = (lat2 * Math.PI) / 180;
394
+ const deltaPhi = ((lat2 - lat1) * Math.PI) / 180;
395
+ const deltaLambda = ((lon2 - lon1) * Math.PI) / 180;
396
+ const a =
397
+ Math.sin(deltaPhi / 2) ** 2 +
398
+ Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda / 2) ** 2;
399
+ return radius * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
400
+ }
package/src/types.ts ADDED
@@ -0,0 +1,201 @@
1
+ export type TransitMode = "subway" | "bus" | "lirr" | "metro-north";
2
+
3
+ export type Direction = "north" | "south" | "east" | "west" | "unknown";
4
+
5
+ export interface MTAOptions {
6
+ busTimeKey?: string;
7
+ databaseUrl?: string;
8
+ databaseAuthToken?: string;
9
+ databaseLocalPath?: string;
10
+ realtimeCacheTtlMs?: number;
11
+ fetch?: typeof fetch;
12
+ now?: () => Date;
13
+ staticData?: StaticGtfsSeed;
14
+ endpoints?: Partial<MTAEndpoints>;
15
+ }
16
+
17
+ export interface MTAEndpoints {
18
+ subwayFeeds: Record<string, string>;
19
+ alerts: string;
20
+ busVehicleMonitoring: string;
21
+ busStopMonitoring: string;
22
+ }
23
+
24
+ export interface StaticGtfsSeed {
25
+ stops?: GtfsStopInput[];
26
+ routes?: GtfsRouteInput[];
27
+ trips?: GtfsTripInput[];
28
+ stopTimes?: GtfsStopTimeInput[];
29
+ }
30
+
31
+ export interface GtfsImportSummary {
32
+ mode: TransitMode;
33
+ sourceUrl?: string;
34
+ importedAt: string;
35
+ stopCount: number;
36
+ routeCount: number;
37
+ tripCount: number;
38
+ stopTimeCount: number;
39
+ }
40
+
41
+ export interface StaticGtfsImportLimits {
42
+ stops?: number;
43
+ routes?: number;
44
+ trips?: number;
45
+ stopTimes?: number;
46
+ }
47
+
48
+ export type StaticGtfsImportStrategy = "core" | "schedule";
49
+
50
+ export interface StaticGtfsImportOptions {
51
+ strategy?: StaticGtfsImportStrategy;
52
+ limits?: StaticGtfsImportLimits;
53
+ rehydrate?: boolean;
54
+ }
55
+
56
+ export interface StaticDataStatus {
57
+ mode: TransitMode;
58
+ ready: boolean;
59
+ importedAt?: string;
60
+ sourceUrl?: string;
61
+ stopCount: number;
62
+ routeCount: number;
63
+ tripCount: number;
64
+ stopTimeCount: number;
65
+ }
66
+
67
+ export type DatabaseStatus = Record<TransitMode, StaticDataStatus>;
68
+
69
+ export interface GtfsStopInput {
70
+ stop_id: string;
71
+ stop_name: string;
72
+ stop_lat?: number | string;
73
+ stop_lon?: number | string;
74
+ parent_station?: string;
75
+ location_type?: number | string;
76
+ }
77
+
78
+ export interface GtfsRouteInput {
79
+ route_id: string;
80
+ route_short_name?: string;
81
+ route_long_name?: string;
82
+ route_type?: number | string;
83
+ route_color?: string;
84
+ route_text_color?: string;
85
+ }
86
+
87
+ export interface GtfsTripInput {
88
+ route_id: string;
89
+ service_id?: string;
90
+ trip_id: string;
91
+ trip_headsign?: string;
92
+ direction_id?: number | string;
93
+ }
94
+
95
+ export interface GtfsStopTimeInput {
96
+ trip_id: string;
97
+ arrival_time?: string;
98
+ departure_time?: string;
99
+ stop_id: string;
100
+ stop_sequence?: number | string;
101
+ }
102
+
103
+ export interface Route {
104
+ id: string;
105
+ shortName?: string;
106
+ longName?: string;
107
+ color?: string;
108
+ textColor?: string;
109
+ type?: number;
110
+ }
111
+
112
+ export interface Stop {
113
+ id: string;
114
+ name: string;
115
+ lat?: number;
116
+ lon?: number;
117
+ parentStation?: string;
118
+ mode?: TransitMode;
119
+ }
120
+
121
+ export interface Arrival {
122
+ mode: TransitMode;
123
+ route: Route;
124
+ stop: Stop;
125
+ direction: Direction;
126
+ headsign?: string;
127
+ arrivalTime: string;
128
+ departureTime?: string;
129
+ minutes: number;
130
+ tripId?: string;
131
+ realtime: boolean;
132
+ source: "mta-gtfs-rt" | "mta-bustime";
133
+ raw?: unknown;
134
+ }
135
+
136
+ export interface Vehicle {
137
+ mode: TransitMode;
138
+ route: Route;
139
+ vehicleId?: string;
140
+ tripId?: string;
141
+ stop?: Stop;
142
+ lat?: number;
143
+ lon?: number;
144
+ bearing?: number;
145
+ destinationName?: string;
146
+ recordedAt?: string;
147
+ source: "mta-bustime";
148
+ raw?: unknown;
149
+ }
150
+
151
+ export interface Alert {
152
+ id: string;
153
+ mode?: TransitMode;
154
+ routes: Route[];
155
+ stops: Stop[];
156
+ header?: string;
157
+ description?: string;
158
+ url?: string;
159
+ effect?: string;
160
+ severity?: string;
161
+ activePeriods: { start?: string; end?: string }[];
162
+ source: "mta-gtfs-rt";
163
+ raw?: unknown;
164
+ }
165
+
166
+ export interface SubwayArrivalQuery {
167
+ stopId: string;
168
+ route?: string;
169
+ direction?: Direction | "uptown" | "downtown";
170
+ limit?: number;
171
+ includeRaw?: boolean;
172
+ }
173
+
174
+ export interface BusArrivalQuery {
175
+ stopId: string;
176
+ route?: string;
177
+ limit?: number;
178
+ includeRaw?: boolean;
179
+ }
180
+
181
+ export interface BusVehicleQuery {
182
+ route?: string;
183
+ vehicleId?: string;
184
+ limit?: number;
185
+ includeRaw?: boolean;
186
+ }
187
+
188
+ export interface AlertQuery {
189
+ mode?: TransitMode;
190
+ route?: string;
191
+ stopId?: string;
192
+ includeRaw?: boolean;
193
+ }
194
+
195
+ export interface StopsNearQuery {
196
+ lat: number;
197
+ lon: number;
198
+ modes?: TransitMode[];
199
+ radiusMeters?: number;
200
+ limit?: number;
201
+ }