minotor 3.0.2 → 4.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.
@@ -13442,11 +13442,16 @@ class Route {
13442
13442
  * @returns The pick-up type at the specified stop and trip.
13443
13443
  */
13444
13444
  pickUpTypeFrom(stopId, tripIndex) {
13445
- const pickUpIndex = (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2;
13446
- const pickUpValue = this.pickUpDropOffTypes[pickUpIndex];
13447
- if (pickUpValue === undefined) {
13445
+ const globalIndex = tripIndex * this.stops.length + this.stopIndex(stopId);
13446
+ const byteIndex = Math.floor(globalIndex / 2);
13447
+ const isSecondPair = globalIndex % 2 === 1;
13448
+ const byte = this.pickUpDropOffTypes[byteIndex];
13449
+ if (byte === undefined) {
13448
13450
  throw new Error(`Pick up type not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`);
13449
13451
  }
13452
+ const pickUpValue = isSecondPair
13453
+ ? (byte >> 6) & 0x03 // Upper 2 bits for second pair
13454
+ : (byte >> 2) & 0x03; // Bits 2-3 for first pair
13450
13455
  return toPickupDropOffType(pickUpValue);
13451
13456
  }
13452
13457
  /**
@@ -13457,11 +13462,16 @@ class Route {
13457
13462
  * @returns The drop-off type at the specified stop and trip.
13458
13463
  */
13459
13464
  dropOffTypeAt(stopId, tripIndex) {
13460
- const dropOffIndex = (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2 + 1;
13461
- const dropOffValue = this.pickUpDropOffTypes[dropOffIndex];
13462
- if (dropOffValue === undefined) {
13465
+ const globalIndex = tripIndex * this.stops.length + this.stopIndex(stopId);
13466
+ const byteIndex = Math.floor(globalIndex / 2);
13467
+ const isSecondPair = globalIndex % 2 === 1;
13468
+ const byte = this.pickUpDropOffTypes[byteIndex];
13469
+ if (byte === undefined) {
13463
13470
  throw new Error(`Drop off type not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`);
13464
13471
  }
13472
+ const dropOffValue = isSecondPair
13473
+ ? (byte >> 4) & 0x03 // Bits 4-5 for second pair
13474
+ : byte & 0x03; // Lower 2 bits for first pair
13465
13475
  return toPickupDropOffType(dropOffValue);
13466
13476
  }
13467
13477
  /**
@@ -13763,7 +13773,7 @@ const ALL_TRANSPORT_MODES = new Set([
13763
13773
  'TROLLEYBUS',
13764
13774
  'MONORAIL',
13765
13775
  ]);
13766
- const CURRENT_VERSION = '0.0.3';
13776
+ const CURRENT_VERSION = '0.0.4';
13767
13777
  /**
13768
13778
  * The internal transit timetable format.
13769
13779
  */
@@ -16194,6 +16204,86 @@ const parseGtfsTransferType = (gtfsTransferType) => {
16194
16204
  }
16195
16205
  };
16196
16206
 
16207
+ /**
16208
+ * Encodes pickup/drop-off types into a Uint8Array using 2 bits per value.
16209
+ * Layout per byte: [drop_off_1][pickup_1][drop_off_0][pickup_0] for stops 0 and 1
16210
+ */
16211
+ const encodePickUpDropOffTypes = (pickUpTypes, dropOffTypes) => {
16212
+ const stopsCount = pickUpTypes.length;
16213
+ // Each byte stores 2 pickup/drop-off pairs (4 bits each)
16214
+ const arraySize = Math.ceil(stopsCount / 2);
16215
+ const encoded = new Uint8Array(arraySize);
16216
+ for (let i = 0; i < stopsCount; i++) {
16217
+ const byteIndex = Math.floor(i / 2);
16218
+ const isSecondPair = i % 2 === 1;
16219
+ const dropOffType = dropOffTypes[i];
16220
+ const pickUpType = pickUpTypes[i];
16221
+ if (dropOffType !== undefined &&
16222
+ pickUpType !== undefined &&
16223
+ byteIndex < encoded.length) {
16224
+ if (isSecondPair) {
16225
+ // Second pair: upper 4 bits
16226
+ const currentByte = encoded[byteIndex];
16227
+ if (currentByte !== undefined) {
16228
+ encoded[byteIndex] =
16229
+ currentByte | (dropOffType << 4) | (pickUpType << 6);
16230
+ }
16231
+ }
16232
+ else {
16233
+ // First pair: lower 4 bits
16234
+ const currentByte = encoded[byteIndex];
16235
+ if (currentByte !== undefined) {
16236
+ encoded[byteIndex] = currentByte | dropOffType | (pickUpType << 2);
16237
+ }
16238
+ }
16239
+ }
16240
+ }
16241
+ return encoded;
16242
+ };
16243
+ /**
16244
+ * Sorts trips by departure time and creates optimized typed arrays
16245
+ */
16246
+ const finalizeRouteFromBuilder = (builder) => {
16247
+ builder.trips.sort((a, b) => a.firstDeparture - b.firstDeparture);
16248
+ const stopsCount = builder.stops.length;
16249
+ const tripsCount = builder.trips.length;
16250
+ const stopsArray = new Uint32Array(builder.stops);
16251
+ const stopTimesArray = new Uint16Array(stopsCount * tripsCount * 2);
16252
+ const allPickUpTypes = [];
16253
+ const allDropOffTypes = [];
16254
+ for (let tripIndex = 0; tripIndex < tripsCount; tripIndex++) {
16255
+ const trip = builder.trips[tripIndex];
16256
+ if (!trip) {
16257
+ throw new Error(`Missing trip data at index ${tripIndex}`);
16258
+ }
16259
+ const baseIndex = tripIndex * stopsCount * 2;
16260
+ for (let stopIndex = 0; stopIndex < stopsCount; stopIndex++) {
16261
+ const timeIndex = baseIndex + stopIndex * 2;
16262
+ const arrivalTime = trip.arrivalTimes[stopIndex];
16263
+ const departureTime = trip.departureTimes[stopIndex];
16264
+ const pickUpType = trip.pickUpTypes[stopIndex];
16265
+ const dropOffType = trip.dropOffTypes[stopIndex];
16266
+ if (arrivalTime === undefined ||
16267
+ departureTime === undefined ||
16268
+ pickUpType === undefined ||
16269
+ dropOffType === undefined) {
16270
+ throw new Error(`Missing trip data for trip ${tripIndex} at stop ${stopIndex}`);
16271
+ }
16272
+ stopTimesArray[timeIndex] = arrivalTime;
16273
+ stopTimesArray[timeIndex + 1] = departureTime;
16274
+ allDropOffTypes.push(dropOffType);
16275
+ allPickUpTypes.push(pickUpType);
16276
+ }
16277
+ }
16278
+ // Use 2-bit encoding for pickup/drop-off types
16279
+ const pickUpDropOffTypesArray = encodePickUpDropOffTypes(allPickUpTypes, allDropOffTypes);
16280
+ return {
16281
+ serviceRouteId: builder.serviceRouteId,
16282
+ stops: stopsArray,
16283
+ stopTimes: stopTimesArray,
16284
+ pickUpDropOffTypes: pickUpDropOffTypesArray,
16285
+ };
16286
+ };
16197
16287
  /**
16198
16288
  * Parses the trips.txt file from a GTFS feed
16199
16289
  *
@@ -16271,11 +16361,11 @@ const parseStopTimes = (stopTimesStream, stopsMap, validTripIds, validStopIds) =
16271
16361
  var _a, e_2, _b, _c;
16272
16362
  var _d, _e;
16273
16363
  /**
16274
- * Inserts a trip at the right place in the routes adjacency structure.
16364
+ * Adds a trip to the appropriate route builder
16275
16365
  */
16276
16366
  const addTrip = (currentTripId) => {
16277
16367
  const gtfsRouteId = validTripIds.get(currentTripId);
16278
- if (!gtfsRouteId) {
16368
+ if (!gtfsRouteId || stops.length === 0) {
16279
16369
  stops = [];
16280
16370
  arrivalTimes = [];
16281
16371
  departureTimes = [];
@@ -16284,83 +16374,42 @@ const parseStopTimes = (stopTimesStream, stopsMap, validTripIds, validStopIds) =
16284
16374
  return;
16285
16375
  }
16286
16376
  const routeId = `${gtfsRouteId}_${hashIds(stops)}`;
16287
- let route = routes.get(routeId);
16288
- if (!route) {
16289
- const stopsCount = stops.length;
16290
- const stopsArray = new Uint32Array(stops);
16291
- const stopTimesArray = new Uint16Array(stopsCount * 2);
16292
- for (let i = 0; i < stopsCount; i++) {
16293
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
16294
- stopTimesArray[i * 2] = arrivalTimes[i];
16295
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
16296
- stopTimesArray[i * 2 + 1] = departureTimes[i];
16297
- }
16298
- const pickUpDropOffTypesArray = new Uint8Array(stopsCount * 2);
16299
- for (let i = 0; i < stopsCount; i++) {
16300
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
16301
- pickUpDropOffTypesArray[i * 2] = pickUpTypes[i];
16302
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
16303
- pickUpDropOffTypesArray[i * 2 + 1] = dropOffTypes[i];
16304
- }
16305
- route = {
16377
+ const firstDeparture = departureTimes[0];
16378
+ if (firstDeparture === undefined) {
16379
+ console.warn(`Empty trip ${currentTripId}`);
16380
+ stops = [];
16381
+ arrivalTimes = [];
16382
+ departureTimes = [];
16383
+ pickUpTypes = [];
16384
+ dropOffTypes = [];
16385
+ return;
16386
+ }
16387
+ let routeBuilder = routeBuilders.get(routeId);
16388
+ if (!routeBuilder) {
16389
+ routeBuilder = {
16306
16390
  serviceRouteId: gtfsRouteId,
16307
- stops: stopsArray,
16308
- stopTimes: stopTimesArray,
16309
- pickUpDropOffTypes: pickUpDropOffTypesArray,
16391
+ stops: [...stops],
16392
+ trips: [],
16310
16393
  };
16311
- routes.set(routeId, route);
16394
+ routeBuilders.set(routeId, routeBuilder);
16312
16395
  for (const stop of stops) {
16313
16396
  validStopIds.add(stop);
16314
16397
  }
16315
16398
  }
16316
- else {
16317
- const tripFirstStopDeparture = departureTimes[0];
16318
- if (tripFirstStopDeparture === undefined) {
16319
- throw new Error(`Empty trip ${currentTripId}`);
16320
- }
16321
- // Find the correct position to insert the new trip
16322
- const stopsCount = stops.length;
16323
- let insertPosition = 0;
16324
- const existingTripsCount = route.stopTimes.length / (stopsCount * 2);
16325
- for (let tripIndex = 0; tripIndex < existingTripsCount; tripIndex++) {
16326
- const currentDeparture = route.stopTimes[tripIndex * stopsCount * 2 + 1];
16327
- if (currentDeparture && tripFirstStopDeparture > currentDeparture) {
16328
- insertPosition = (tripIndex + 1) * stopsCount;
16329
- }
16330
- else {
16331
- break;
16332
- }
16333
- }
16334
- // insert data for the new trip at the right place
16335
- const newStopTimesLength = route.stopTimes.length + stopsCount * 2;
16336
- const newStopTimes = new Uint16Array(newStopTimesLength);
16337
- const newPickUpDropOffTypes = new Uint8Array(newStopTimesLength);
16338
- newStopTimes.set(route.stopTimes.slice(0, insertPosition * 2), 0);
16339
- newPickUpDropOffTypes.set(route.pickUpDropOffTypes.slice(0, insertPosition * 2), 0);
16340
- for (let i = 0; i < stopsCount; i++) {
16341
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
16342
- newStopTimes[(insertPosition + i) * 2] = arrivalTimes[i];
16343
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
16344
- newStopTimes[(insertPosition + i) * 2 + 1] = departureTimes[i];
16345
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
16346
- newPickUpDropOffTypes[(insertPosition + i) * 2] = pickUpTypes[i];
16347
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
16348
- newPickUpDropOffTypes[(insertPosition + i) * 2 + 1] = dropOffTypes[i];
16349
- }
16350
- const afterInsertionSlice = route.stopTimes.slice(insertPosition * 2);
16351
- newStopTimes.set(afterInsertionSlice, (insertPosition + stopsCount) * 2);
16352
- const afterInsertionTypesSlice = route.pickUpDropOffTypes.slice(insertPosition * 2);
16353
- newPickUpDropOffTypes.set(afterInsertionTypesSlice, (insertPosition + stopsCount) * 2);
16354
- route.stopTimes = newStopTimes;
16355
- route.pickUpDropOffTypes = newPickUpDropOffTypes;
16356
- }
16399
+ routeBuilder.trips.push({
16400
+ firstDeparture,
16401
+ arrivalTimes: [...arrivalTimes],
16402
+ departureTimes: [...departureTimes],
16403
+ pickUpTypes: [...pickUpTypes],
16404
+ dropOffTypes: [...dropOffTypes],
16405
+ });
16357
16406
  stops = [];
16358
16407
  arrivalTimes = [];
16359
16408
  departureTimes = [];
16360
16409
  pickUpTypes = [];
16361
16410
  dropOffTypes = [];
16362
16411
  };
16363
- const routes = new Map();
16412
+ const routeBuilders = new Map();
16364
16413
  let previousSeq = 0;
16365
16414
  let stops = [];
16366
16415
  let arrivalTimes = [];
@@ -16375,27 +16424,33 @@ const parseStopTimes = (stopTimesStream, stopsMap, validTripIds, validStopIds) =
16375
16424
  const rawLine = _c;
16376
16425
  const line = rawLine;
16377
16426
  if (line.trip_id === currentTripId && line.stop_sequence <= previousSeq) {
16378
- console.warn(`Stop sequences not increasing for trip ${line.trip_id}.`);
16427
+ console.warn(`Stop sequences not increasing for trip ${line.trip_id}: ${line.stop_sequence} > ${previousSeq}.`);
16379
16428
  continue;
16380
16429
  }
16381
16430
  if (!line.arrival_time && !line.departure_time) {
16382
16431
  console.warn(`Missing arrival or departure time for ${line.trip_id} at stop ${line.stop_id}.`);
16383
16432
  continue;
16384
16433
  }
16385
- if (line.pickup_type === 1 && line.drop_off_type === 1) {
16434
+ if (line.pickup_type === '1' && line.drop_off_type === '1') {
16386
16435
  continue;
16387
16436
  }
16388
16437
  if (currentTripId && line.trip_id !== currentTripId && stops.length > 0) {
16389
16438
  addTrip(currentTripId);
16390
16439
  }
16391
16440
  currentTripId = line.trip_id;
16392
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
16393
- stops.push(stopsMap.get(line.stop_id).id);
16441
+ const stopData = stopsMap.get(line.stop_id);
16442
+ if (!stopData) {
16443
+ console.warn(`Unknown stop ID: ${line.stop_id}`);
16444
+ continue;
16445
+ }
16446
+ stops.push(stopData.id);
16394
16447
  const departure = (_d = line.departure_time) !== null && _d !== void 0 ? _d : line.arrival_time;
16395
16448
  const arrival = (_e = line.arrival_time) !== null && _e !== void 0 ? _e : line.departure_time;
16396
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
16449
+ if (!arrival || !departure) {
16450
+ console.warn(`Missing time data for ${line.trip_id} at stop ${line.stop_id}`);
16451
+ continue;
16452
+ }
16397
16453
  arrivalTimes.push(toTime(arrival).toMinutes());
16398
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
16399
16454
  departureTimes.push(toTime(departure).toMinutes());
16400
16455
  pickUpTypes.push(parsePickupDropOffType(line.pickup_type));
16401
16456
  dropOffTypes.push(parsePickupDropOffType(line.drop_off_type));
@@ -16413,7 +16468,8 @@ const parseStopTimes = (stopTimesStream, stopsMap, validTripIds, validStopIds) =
16413
16468
  addTrip(currentTripId);
16414
16469
  }
16415
16470
  const routesAdjacency = new Map();
16416
- for (const [routeId, routeData] of routes) {
16471
+ for (const [routeId, routeBuilder] of routeBuilders) {
16472
+ const routeData = finalizeRouteFromBuilder(routeBuilder);
16417
16473
  routesAdjacency.set(routeId, new Route(routeData.stopTimes, routeData.pickUpDropOffTypes, routeData.stops, routeData.serviceRouteId));
16418
16474
  }
16419
16475
  return routesAdjacency;
@@ -16422,13 +16478,13 @@ const parsePickupDropOffType = (gtfsType) => {
16422
16478
  switch (gtfsType) {
16423
16479
  default:
16424
16480
  return REGULAR;
16425
- case 0:
16481
+ case '0':
16426
16482
  return REGULAR;
16427
- case 1:
16483
+ case '1':
16428
16484
  return NOT_AVAILABLE;
16429
- case 2:
16485
+ case '2':
16430
16486
  return MUST_PHONE_AGENCY;
16431
- case 3:
16487
+ case '3':
16432
16488
  return MUST_COORDINATE_WITH_DRIVER;
16433
16489
  }
16434
16490
  };
@@ -16456,54 +16512,74 @@ class GtfsParser {
16456
16512
  */
16457
16513
  parse(date) {
16458
16514
  return __awaiter(this, void 0, void 0, function* () {
16515
+ log.setLevel('INFO');
16459
16516
  const zip = new StreamZip.async({ file: this.path });
16460
16517
  const entries = yield zip.entries();
16461
16518
  const datetime = DateTime.fromJSDate(date);
16462
16519
  const validServiceIds = new Set();
16463
16520
  const validStopIds = new Set();
16464
16521
  log.info(`Parsing ${STOPS_FILE}`);
16522
+ const stopsStart = performance.now();
16465
16523
  const stopsStream = yield zip.stream(STOPS_FILE);
16466
16524
  const parsedStops = yield parseStops(stopsStream, this.profile.platformParser);
16467
- log.info(`${parsedStops.size} parsed stops.`);
16525
+ const stopsEnd = performance.now();
16526
+ log.info(`${parsedStops.size} parsed stops. (${(stopsEnd - stopsStart).toFixed(2)}ms)`);
16468
16527
  if (entries[CALENDAR_FILE]) {
16469
16528
  log.info(`Parsing ${CALENDAR_FILE}`);
16529
+ const calendarStart = performance.now();
16470
16530
  const calendarStream = yield zip.stream(CALENDAR_FILE);
16471
16531
  yield parseCalendar(calendarStream, validServiceIds, datetime);
16472
- log.info(`${validServiceIds.size} valid services.`);
16532
+ const calendarEnd = performance.now();
16533
+ log.info(`${validServiceIds.size} valid services. (${(calendarEnd - calendarStart).toFixed(2)}ms)`);
16473
16534
  }
16474
16535
  if (entries[CALENDAR_DATES_FILE]) {
16475
16536
  log.info(`Parsing ${CALENDAR_DATES_FILE}`);
16537
+ const calendarDatesStart = performance.now();
16476
16538
  const calendarDatesStream = yield zip.stream(CALENDAR_DATES_FILE);
16477
16539
  yield parseCalendarDates(calendarDatesStream, validServiceIds, datetime);
16478
- log.info(`${validServiceIds.size} valid services.`);
16540
+ const calendarDatesEnd = performance.now();
16541
+ log.info(`${validServiceIds.size} valid services. (${(calendarDatesEnd - calendarDatesStart).toFixed(2)}ms)`);
16479
16542
  }
16480
16543
  log.info(`Parsing ${ROUTES_FILE}`);
16544
+ const routesStart = performance.now();
16481
16545
  const routesStream = yield zip.stream(ROUTES_FILE);
16482
16546
  const validGtfsRoutes = yield parseRoutes(routesStream, this.profile);
16483
- log.info(`${validGtfsRoutes.size} valid GTFS routes.`);
16547
+ const routesEnd = performance.now();
16548
+ log.info(`${validGtfsRoutes.size} valid GTFS routes. (${(routesEnd - routesStart).toFixed(2)}ms)`);
16484
16549
  log.info(`Parsing ${TRIPS_FILE}`);
16550
+ const tripsStart = performance.now();
16485
16551
  const tripsStream = yield zip.stream(TRIPS_FILE);
16486
16552
  const trips = yield parseTrips(tripsStream, validServiceIds, validGtfsRoutes);
16487
- log.info(`${trips.size} valid trips.`);
16553
+ const tripsEnd = performance.now();
16554
+ log.info(`${trips.size} valid trips. (${(tripsEnd - tripsStart).toFixed(2)}ms)`);
16488
16555
  let transfers = new Map();
16489
16556
  if (entries[TRANSFERS_FILE]) {
16490
16557
  log.info(`Parsing ${TRANSFERS_FILE}`);
16558
+ const transfersStart = performance.now();
16491
16559
  const transfersStream = yield zip.stream(TRANSFERS_FILE);
16492
16560
  transfers = yield parseTransfers(transfersStream, parsedStops);
16493
- log.info(`${transfers.size} valid transfers.`);
16561
+ const transfersEnd = performance.now();
16562
+ log.info(`${transfers.size} valid transfers. (${(transfersEnd - transfersStart).toFixed(2)}ms)`);
16494
16563
  }
16495
16564
  log.info(`Parsing ${STOP_TIMES_FILE}`);
16565
+ const stopTimesStart = performance.now();
16496
16566
  const stopTimesStream = yield zip.stream(STOP_TIMES_FILE);
16497
16567
  const routesAdjacency = yield parseStopTimes(stopTimesStream, parsedStops, trips, validStopIds);
16498
16568
  const stopsAdjacency = buildStopsAdjacencyStructure(validStopIds, routesAdjacency, transfers);
16499
- log.info(`${routesAdjacency.size} valid unique routes.`);
16569
+ const stopTimesEnd = performance.now();
16570
+ log.info(`${routesAdjacency.size} valid unique routes. (${(stopTimesEnd - stopTimesStart).toFixed(2)}ms)`);
16500
16571
  log.info(`Removing unused stops.`);
16572
+ const indexStopsStart = performance.now();
16501
16573
  const stops = indexStops(parsedStops, validStopIds);
16502
- log.info(`${stops.size} used stop stops, ${parsedStops.size - stops.size} unused.`);
16574
+ const indexStopsEnd = performance.now();
16575
+ log.info(`${stops.size} used stop stops, ${parsedStops.size - stops.size} unused. (${(indexStopsEnd - indexStopsStart).toFixed(2)}ms)`);
16503
16576
  yield zip.close();
16504
16577
  const timetable = new Timetable(stopsAdjacency, routesAdjacency, validGtfsRoutes);
16505
16578
  log.info(`Building stops index.`);
16579
+ const stopsIndexStart = performance.now();
16506
16580
  const stopsIndex = new StopsIndex(stops);
16581
+ const stopsIndexEnd = performance.now();
16582
+ log.info(`Stops index built. (${(stopsIndexEnd - stopsIndexStart).toFixed(2)}ms)`);
16507
16583
  log.info('Parsing complete.');
16508
16584
  return { timetable, stopsIndex };
16509
16585
  });
@@ -16519,9 +16595,11 @@ class GtfsParser {
16519
16595
  return __awaiter(this, void 0, void 0, function* () {
16520
16596
  const zip = new StreamZip.async({ file: this.path });
16521
16597
  log.info(`Parsing ${STOPS_FILE}`);
16598
+ const stopsStart = performance.now();
16522
16599
  const stopsStream = yield zip.stream(STOPS_FILE);
16523
16600
  const stops = indexStops(yield parseStops(stopsStream, this.profile.platformParser));
16524
- log.info(`${stops.size} parsed stops.`);
16601
+ const stopsEnd = performance.now();
16602
+ log.info(`${stops.size} parsed stops. (${(stopsEnd - stopsStart).toFixed(2)}ms)`);
16525
16603
  yield zip.close();
16526
16604
  return new StopsIndex(stops);
16527
16605
  });