universal-parcel-scraper 0.1024.0-main.5559 → 0.1024.2-main.5565

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
@@ -164,7 +164,7 @@ the command line's `track --source ParcelsApp` does the same.
164
164
  <!-- GENERATED:stages -->
165
165
  <img src="docs/assets/stages.svg" alt="DHL: Die Sendung wurde in das Zustellfahrzeug geladen.; Mondial Relay: En cours de livraison; Correios Brazil: Objeto saiu para entrega ao destinatário; Correos Express: EN REPARTO; Yamato Transport: 配達中; La Poste / Colissimo: DISTOU. All are filed under out_for_delivery." width="760">
166
166
 
167
- The carrier folders record 19,305 such statuses, each filed under one stage.
167
+ The carrier folders record 19,308 such statuses, each filed under one stage.
168
168
  <!-- /GENERATED:stages -->
169
169
 
170
170
  Wording nobody recorded yet goes through a classifier that reads seven European languages.
@@ -47000,7 +47000,7 @@ function Au(e) {
47000
47000
  if (/^acceptance of the (?:package|parcel) at the delivery branch abroad$/.test(t)) return "in_transit";
47001
47001
  if (/^acceptance of the (?:package|parcel) at the pick ?up point$/.test(t) || /^(?:(?:the )?item )?(?:arrival|arrived|receive item) at (?:the )?collection point for pick ?up\b/.test(t)) return "ready_for_pickup";
47002
47002
  if (/^(?:package|parcel) received (?:&|and) scanned at (?:the )?carrier's domestic warehouse$/.test(t) || /^(?:the )?goods (?:arrive|arrived|have arrived) at (?:the )?receiving point$/.test(t) || /(?<!\b(?:no|nao) )\badmitid[oa]s?\b|\badmision (?:en origen|pais de origen)\b/.test(t) || /^aceite(?:\.|$)|\b(?:envio|objeto|objecto|encomenda|remessa) foi aceite\b/.test(t)) return "accepted";
47003
- if (/\bcross ?dock(?:ed|ing)?\b|\bready for outbound\b|^(?:(?:item|parcel|package|shipment) )?inbound scan(?:ned|ning)?\b|\b(?:un)?bagg(?:ed|ing)\b|\bmaster (?:air ?way ?bill|awb)(?: (?:number|no))? (?:received|assigned|allocated)\b|\bhand ?over to (?:the |an )?(?:airline|air carrier)\b|^(?:(?:the |your )?(?:item|parcel|package|shipment) )?(?:has )?left (?:the|our) warehouse$|\b(?:flight|leg)\b[^.;]*\b(?:is taking off|took off|has taken off|landed)\b|\bflight (?:take ?off|landing)\b|^(?:initial|final) landing$|\bcargo tally in the supervised warehouse\b|\bprepared for (?:the )?last[- ]mile hand ?over\b/.test(t) || /^(?:the )?noa (?:has been |was )?received$|^noa-notification of arrival\b/.test(t) || /^(?:the |your )?(?:parcel|package|shipment|item) is (?:now )?at (?:the |a )?distribution cent(?:er|re)$/.test(t) || /^l'envoi est dans le reseau de livraison local$/.test(t) || /^there (?:has|have) been (?:a |an )?(?:slight |short |small |unexpected )?delay (?:with|to|in) (?:your |the )?(?:delivery|parcel|package|shipment|order)\.?$/.test(t) || /transit|\bline ?haul\b|en route|on (?:its|the) way|\bfacility\b|airport|\bleaving\b|\bdispatched\b|^delay(?:ed)?$|received by (?:the )?(?:local )?(?:delivery|logistics) company|\btransfer to (?:the |a |an )?(?:local |destination )?(?:acting |delivery |logistics |courier )?(?:company|agent|carrier|partner)\b|received by (?:postnl|uniuni)\b|notification.*sent to the recipient|delivery option requested|instruction de livraison recue|cambio (?:delle )?disposizioni di consegna|expedie (?:depuis|du|de)\b|acheminement|unterwegs|\bin viaggio\b|\bw drodze\b|\bonderweg\b|transport|trasport|arriv|depart|processed|processing completed|processing at (?:the )?(?:origin |destination )?branch\b|\breceipt of (?:the )?(?:package|parcel|shipment) at (?:the )?dpd branch\b|sorted|sorting|\btri\b|\btriee?s?\b|traitement|traite|sortier|bearbeitet|bearbeitung|elaborat|elaborazion|smistat|parcel cent(?:er|re)|delivery cent(?:er|re)|centre.*(?:colis|distribution)|paketzentrum|verteilzentrum|centro.*(?:pacchi|distribuzione)|depot|import scan|scan.*import|importscan|scansione.*import|delivery.*delayed|livraison.*(?:retard|report)|zustellung.*verzogert|consegna.*ritard|transferred|transfere|weitergeleitet|inoltrato|left.*(?:point|center)|quitte|verlassen|raggiunto|close bag|fermeture du sac|sack.*geschlossen|chiusura.*sacco|scanned into|scanned at (?:a |the )?checkpoint|dismantling the board|scanne dans|eingescannt|scansionat|\b(?:un)?loaded\b|^(?:item|parcel|shipment) distributed$|charg.*vehicule de transport|transportfahrzeug.*geladen|caric.*veicolo.*trasporto/.test(t) || /^enviad[oa]$|\bclasificad[oa]s?\b|\bclasificacion\b|\ben ruta\b|\ben camino\b|\bcargad[oa]s? en (?:el )?vehiculo\b|\bllegad[oa]s? (?:a|al|en)\b|\bllegada (?:a|al|en|de)\b|\bha llegado\b|\bllego (?:a|al)\b|\bsalida (?:de|del|desde)\b|\b(?:ha )?salido (?:de|del)\b|\bsalio (?:de|del)\b|\ben destino\b|\b(?:agencia|delegacion|plataforma|oficina|franquicia) (?:de )?destino\b|\b(?:unidad|centro|oficina|delegacion|base|agencia|plataforma) de reparto\b|\bcentro (?:de )?(?:clasificacion|distribucion|tratamiento|logistico|operaciones|operativo)\b|\blectura\b|\bleid[oa]s?\b|\brecepcionad[oa]s?\b|\bprocesad[oa]s?\b|\bexpedid[oa]s?\b|\btriagem\b|\btriad[oa]s?\b|\bem tratamento\b|\btratad[oa]s?\b|\bcentro (?:de )?(?:distribuicao|tratamento|triagem|operacional)\b|\bchegad[ao] (?:a|ao|no|na|em)\b|\bchegou (?:a|ao|no|na|em)\b|\bsaida (?:do|da|de|dos|das)\b|\bsaiu (?:do|da|de|dos|das)\b|\bem rota\b|\ba caminho\b|\bencaminhad[oa]s?\b|\b(?:em )?transferencia\b|\btransferid[oa]s?\b|\bprocessad[oa]s?\b|\brecebid[oa] (?:no|na|em) (?:centro|armazem|unidade|plataforma)\b|\brecibid[oa] en (?:el |la |nuestr[oa] )?(?:almacen|centro|delegacion|plataforma|hub|agencia)\b/.test(t)) return "in_transit";
47003
+ if (/\bcross ?dock(?:ed|ing)?\b|\bready for outbound\b|^(?:(?:item|parcel|package|shipment) )?inbound scan(?:ned|ning)?\b|\b(?:un)?bagg(?:ed|ing)\b|\bmaster (?:air ?way ?bill|awb)(?: (?:number|no))? (?:received|assigned|allocated)\b|\bhand ?over to (?:the |an )?(?:airline|air carrier)\b|^(?:(?:the |your )?(?:item|parcel|package|shipment) )?(?:has )?left (?:the|our) warehouse$|\b(?:flight|leg)\b[^.;]*\b(?:is taking off|took off|has taken off|has left|landed)\b|\bflight (?:take ?off|landing)\b|^(?:initial|final) landing$|\bcargo tally in the supervised warehouse\b|\bprepared for (?:the )?last[- ]mile hand ?over\b/.test(t) || /^(?:the )?noa (?:has been |was )?received$|^noa-notification of arrival\b/.test(t) || /^(?:the |your )?(?:parcel|package|shipment|item) is (?:now )?at (?:the |a )?distribution cent(?:er|re)$/.test(t) || /^l'envoi est dans le reseau de livraison local$/.test(t) || /^there (?:has|have) been (?:a |an )?(?:slight |short |small |unexpected )?delay (?:with|to|in) (?:your |the )?(?:delivery|parcel|package|shipment|order)\.?$/.test(t) || /transit|\bline ?haul\b|en route|on (?:its|the) way|\bfacility\b|airport|\bleaving\b|\bdispatched\b|^delay(?:ed)?$|received by (?:the )?(?:local )?(?:delivery|logistics) company|\btransfer to (?:the |a |an )?(?:local |destination )?(?:acting |delivery |logistics |courier )?(?:company|agent|carrier|partner)\b|received by (?:postnl|uniuni)\b|notification.*sent to the recipient|delivery option requested|instruction de livraison recue|cambio (?:delle )?disposizioni di consegna|expedie (?:depuis|du|de)\b|acheminement|unterwegs|\bin viaggio\b|\bw drodze\b|\bonderweg\b|transport|trasport|arriv|depart|processed|processing completed|processing at (?:the )?(?:origin |destination )?branch\b|\breceipt of (?:the )?(?:package|parcel|shipment) at (?:the )?dpd branch\b|sorted|sorting|\btri\b|\btriee?s?\b|traitement|traite|sortier|bearbeitet|bearbeitung|elaborat|elaborazion|smistat|parcel cent(?:er|re)|delivery cent(?:er|re)|centre.*(?:colis|distribution)|paketzentrum|verteilzentrum|centro.*(?:pacchi|distribuzione)|depot|import scan|scan.*import|importscan|scansione.*import|delivery.*delayed|livraison.*(?:retard|report)|zustellung.*verzogert|consegna.*ritard|transferred|transfere|weitergeleitet|inoltrato|left.*(?:point|center)|quitte|verlassen|raggiunto|close bag|fermeture du sac|sack.*geschlossen|chiusura.*sacco|scanned into|scanned at (?:a |the )?checkpoint|dismantling the board|scanne dans|eingescannt|scansionat|\b(?:un)?loaded\b|^(?:item|parcel|shipment) distributed$|charg.*vehicule de transport|transportfahrzeug.*geladen|caric.*veicolo.*trasporto/.test(t) || /^enviad[oa]$|\bclasificad[oa]s?\b|\bclasificacion\b|\ben ruta\b|\ben camino\b|\bcargad[oa]s? en (?:el )?vehiculo\b|\bllegad[oa]s? (?:a|al|en)\b|\bllegada (?:a|al|en|de)\b|\bha llegado\b|\bllego (?:a|al)\b|\bsalida (?:de|del|desde)\b|\b(?:ha )?salido (?:de|del)\b|\bsalio (?:de|del)\b|\ben destino\b|\b(?:agencia|delegacion|plataforma|oficina|franquicia) (?:de )?destino\b|\b(?:unidad|centro|oficina|delegacion|base|agencia|plataforma) de reparto\b|\bcentro (?:de )?(?:clasificacion|distribucion|tratamiento|logistico|operaciones|operativo)\b|\blectura\b|\bleid[oa]s?\b|\brecepcionad[oa]s?\b|\bprocesad[oa]s?\b|\bexpedid[oa]s?\b|\btriagem\b|\btriad[oa]s?\b|\bem tratamento\b|\btratad[oa]s?\b|\bcentro (?:de )?(?:distribuicao|tratamento|triagem|operacional)\b|\bchegad[ao] (?:a|ao|no|na|em)\b|\bchegou (?:a|ao|no|na|em)\b|\bsaida (?:do|da|de|dos|das)\b|\bsaiu (?:do|da|de|dos|das)\b|\bem rota\b|\ba caminho\b|\bencaminhad[oa]s?\b|\b(?:em )?transferencia\b|\btransferid[oa]s?\b|\bprocessad[oa]s?\b|\brecebid[oa] (?:no|na|em) (?:centro|armazem|unidade|plataforma)\b|\brecibid[oa] en (?:el |la |nuestr[oa] )?(?:almacen|centro|delegacion|plataforma|hub|agencia)\b/.test(t)) return "in_transit";
47004
47004
  if (/^(?:приинято|принято)$/.test(t)) return "accepted";
47005
47005
  if (au.test(t)) return "in_transit";
47006
47006
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The zone of the recipient country's clock, which every scan shows, where iMile
3
+ * delivers on one. iMile names a country, never a state or province, so Peru's
4
+ * code is Peru's. Null for a country whose clock the scans do not say.
5
+ */
6
+ export declare function imileCountryClock(country: string): string | null;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The clock iMile's tracker shows a parcel's scans on, which other feeds relaying
3
+ * them keep (providers/ship24).
4
+ */
5
+ import { countryTimeZone } from '../../core/time/index.js';
6
+ import { countryZones } from '../../places/zones.js';
7
+ /**
8
+ * The clock of a country iMile delivers in on several. New Zealand's other zone is
9
+ * the Chatham Islands': its delivery stations keep Auckland's clock. iMile Australia
10
+ * dates every scan on Sydney's, Queensland's included: on Brisbane's clock, its
11
+ * hubs' arrivals there would come after a consumer first saw them.
12
+ */
13
+ const COUNTRY_CLOCKS = { NZ: 'Pacific/Auckland', AU: 'Australia/Sydney' };
14
+ /**
15
+ * The zone of the recipient country's clock, which every scan shows, where iMile
16
+ * delivers on one. iMile names a country, never a state or province, so Peru's
17
+ * code is Peru's. Null for a country whose clock the scans do not say.
18
+ */
19
+ export function imileCountryClock(country) {
20
+ const zones = countryZones(country);
21
+ return countryTimeZone(country) ?? (zones.length === 1 ? zones[0] : COUNTRY_CLOCKS[country] ?? null);
22
+ }
@@ -2,10 +2,10 @@ import { DateTime } from 'luxon';
2
2
  import { normalizeTrackingNumber } from '../../core/detection/index.js';
3
3
  import { IndeterminateError, InvalidInputError, NoHistoryError, NotFoundError, RateLimitedError, SchemaError } from '../../core/errors/index.js';
4
4
  import { languageStageStatus } from '../../core/status/index.js';
5
- import { calendarDay, countryTimeZone, isoTime } from '../../core/time/index.js';
5
+ import { calendarDay, isoTime } from '../../core/time/index.js';
6
6
  import { clean } from '../../core/transport/index.js';
7
7
  import { isRecord } from '../../core/types.js';
8
- import { countryZones } from '../../places/zones.js';
8
+ import { imileCountryClock } from './clock.js';
9
9
  import { imileStage } from './status.js';
10
10
  import { imileDisplayWording } from './wording.js';
11
11
  export const PROVIDER = 'iMile';
@@ -29,16 +29,6 @@ const COUNTRIES = {
29
29
  HND: 'HN', HUN: 'HU', ITA: 'IT', KWT: 'KW', LBN: 'LB', MEX: 'MX', NZL: 'NZ', MKD: 'MK', OMN: 'OM', PER: 'PE',
30
30
  POL: 'PL', QAT: 'QA', KSA: 'SA', SRB: 'RS', SVK: 'SK', RSA: 'ZA', ESP: 'ES', TUR: 'TR', UAE: 'AE', GBR: 'GB', USA: 'US',
31
31
  };
32
- // New Zealand's other zone is the Chatham Islands': its delivery stations keep Auckland's clock.
33
- const MAINLAND_ZONES = { NZ: 'Pacific/Auckland' };
34
- /**
35
- * The zone of the recipient country's clock, where iMile delivers on one. iMile
36
- * names a country, never a state or province, so Peru's code is Peru's.
37
- */
38
- function countryClock(country) {
39
- const zones = countryZones(country);
40
- return countryTimeZone(country) ?? (zones.length === 1 ? zones[0] : MAINLAND_ZONES[country] ?? null);
41
- }
42
32
  /** A number for iMile's tracker, in upper case; anything else is not one it answers. */
43
33
  export function normalizeImileNumber(raw) {
44
34
  const number = raw.length <= 64 ? normalizeTrackingNumber(raw).toUpperCase() : '';
@@ -175,7 +165,7 @@ export function parseImile(payload, raw) {
175
165
  const country = Object.hasOwn(COUNTRIES, code) ? COUNTRIES[code] : undefined;
176
166
  // Every scan shows the recipient country's clock: a country of one zone
177
167
  // places them all, and one of several leaves them local.
178
- const zone = country ? countryClock(country) : null;
168
+ const zone = country ? imileCountryClock(country) : null;
179
169
  const placed = zone ? newestFirst.map(({ local_time, ...event }) => {
180
170
  const time = isoTime(local_time, zone)?.iso;
181
171
  return time ? { time, ...event } : { local_time, ...event };
@@ -24,7 +24,8 @@ export function yanwenTrackingUrl(raw) {
24
24
  }
25
25
  // Partner scans can carry the delivery postcode: a Canadian one before the
26
26
  // town, a US ZIP after the state. A location keeps its town and region only,
27
- // and a partner's "Not Available" or bare "AA", a code no country holds, is none.
27
+ // and a partner's "Not Available" or bare "AA", a code no country holds, is
28
+ // none, as are a code of one or two digits and the partner's own name.
28
29
  const POSTCODE = /^(?:[A-Z]\d[A-Z] ?\d[A-Z]\d|\d{5}(?:-\d{4})?)$/i;
29
30
  // GOFO's European delivery scans say whether a PIN was used and give a door
30
31
  // number; neither belongs in the wording.
@@ -1,4 +1,7 @@
1
1
  import type { SameInstantIdentityPolicy } from '../../core/catalog/eventIdentity.js';
2
- /** Whether a relayed scan's place is a partner's placeholder ("Not Available"), which names no place. */
2
+ /**
3
+ * Whether a relayed scan's place names none: a partner's placeholder ("Not Available"), a code of
4
+ * one or two digits ("03"), or the partner's own name ("INPOST").
5
+ */
3
6
  export declare function isYanwenPlaceholder(location: string): boolean;
4
7
  export declare const sameInstantIdentityPolicy: SameInstantIdentityPolicy;
@@ -1,10 +1,21 @@
1
- import { matchesDroppedLocation, matchesLocationEnrichment } from '../../core/catalog/locationIdentity.js';
1
+ import { isKnownCarrierName } from '../../core/catalog/hints.js';
2
+ import { matchesLocationEnrichment } from '../../core/catalog/locationIdentity.js';
2
3
  import { yanwenWording } from './status.js';
3
- /** Whether a relayed scan's place is a partner's placeholder ("Not Available"), which names no place. */
4
+ /**
5
+ * Whether a relayed scan's place names none: a partner's placeholder ("Not Available"), a code of
6
+ * one or two digits ("03"), or the partner's own name ("INPOST").
7
+ */
4
8
  export function isYanwenPlaceholder(location) {
5
- return /^(?:not available|AA)$/i.test(location.replace(/\s+/g, ' ').trim());
9
+ const place = location.replace(/\s+/g, ' ').trim();
10
+ return /^(?:not available|AA|\d{1,2})$/i.test(place) || isKnownCarrierName(place);
6
11
  }
7
12
  const same = (value) => value.replace(/\s+/g, ' ').trim().toLowerCase();
13
+ /** A scan stored with a placeholder as its place, which the adapter now drops, at the same instant in the same words, whatever stage it was stored on. */
14
+ function droppedPlaceholder(incoming, stored) {
15
+ return !same(incoming.location) && Boolean(same(stored.location)) && isYanwenPlaceholder(stored.location)
16
+ && Boolean(same(incoming.description)) && same(incoming.description) === same(stored.description)
17
+ && !['', 'unknown'].includes(incoming.stage);
18
+ }
8
19
  /** A J&T Australia scan stored with the customer service line the adapter now leaves out, on the same stage in the same place. */
9
20
  function lostJAndTContact(incoming, stored) {
10
21
  const wording = yanwenWording(stored.description);
@@ -12,11 +23,12 @@ function lostJAndTContact(incoming, stored) {
12
23
  && incoming.stage === stored.stage && same(incoming.location) === same(stored.location);
13
24
  }
14
25
  // A relayed scan stored without a place can take it from the UTC copy the
15
- // adapter now drops, one stored with a placeholder as its place loses it, and
16
- // one stored with J&T Australia's customer service line loses that.
26
+ // adapter now drops, one stored with a placeholder as its place loses it and
27
+ // takes the stage the map now gives notices and handovers that came with one,
28
+ // and one stored with J&T Australia's customer service line loses that.
17
29
  export const sameInstantIdentityPolicy = {
18
30
  sourceCarrierId: 'yanwen', storedSources: ['yanwen'], requireProviderCode: false,
19
31
  matchEachScan: true,
20
- matches: (incoming, stored) => matchesLocationEnrichment(incoming, stored) || matchesDroppedLocation(incoming, stored, isYanwenPlaceholder)
32
+ matches: (incoming, stored) => matchesLocationEnrichment(incoming, stored) || droppedPlaceholder(incoming, stored)
21
33
  || lostJAndTContact(incoming, stored),
22
34
  };
@@ -45,6 +45,7 @@ const WORDING = new Map([
45
45
  ['Arrived at USPS Regional Origin Facility', { status: 'in_transit', stage: 'in_transit' }],
46
46
  ['In Transit to Next Facility, Arriving On Time', { status: 'in_transit', stage: 'in_transit' }],
47
47
  ['Your shipment has been accepted by delivery carrier', { status: 'in_transit', stage: 'in_transit' }],
48
+ ['Parcel Picked Up', { status: 'in_transit', stage: 'in_transit' }],
48
49
  ['Your shipment has arrived at sorting center', { status: 'in_transit', stage: 'in_transit' }],
49
50
  // A partner's manifest as the parcel leaves Yanwen's facility, then the destination leg's
50
51
  // release from customs, intake and arrival at the delivery office.
@@ -189,11 +190,14 @@ export function yanwenStatus(description, code) {
189
190
  }
190
191
  const STAGE_BY_WORDING = new Map([...WORDING].map(([wording, { stage }]) => [normalizeStatusWording(wording), stage]));
191
192
  // USPS's record of the label Yanwen made, relayed while Yanwen still has the parcel in China, at the
192
- // label's US origin, and DHL's record of the recipient's choice of a Packstation: notices that move
193
- // nothing and name no place the parcel has been.
193
+ // label's US origin, DHL's record of the recipient's choice of a Packstation, and InPost's records of
194
+ // the sender's data and of the recipient's delivery choice: notices that move nothing and name no
195
+ // place the parcel has been.
194
196
  const NOTICES = new Map([
195
197
  ['Shipping Label Created, USPS Awaiting Item', "USPS's record of the label Yanwen made for it, relayed while Yanwen still has the parcel: not a scan, so it keeps the stage the parcel already had."],
196
198
  ['The recipient has selected a Packstation as the new delivery address.', "DHL's record of the recipient's choice of a Packstation, relayed while the parcel can still be in China: not a scan, so it keeps the stage the parcel already had."],
199
+ ['Pre-shipment Info sent to carrier', "InPost's record of the sender's data reaching it, relayed while Yanwen still has the parcel: not a scan, so it keeps the stage the parcel already had."],
200
+ ['Preference applied', "InPost's record of the recipient's delivery choice, relayed while the parcel can still be in China: not a scan, so it keeps the stage the parcel already had."],
197
201
  ].map(([wording, note]) => [normalizeStatusWording(wording), note]));
198
202
  /** Whether a scan is a notice: it keeps the stage of the scan before it and names no place. */
199
203
  export const yanwenNotice = (description) => NOTICES.has(normalizeStatusWording(withoutPrefix(description)));
@@ -203,6 +203,20 @@
203
203
  "firstSeen": "2026-11-07",
204
204
  "note": "Relayed from DHL a minute after DHL's record of the sender's data, while the parcel is still in China: the recipient's instruction, a notice that keeps the stage before it."
205
205
  },
206
+ {
207
+ "wording": "Pre-shipment Info sent to carrier",
208
+ "language": "en",
209
+ "confirmedBy": "live",
210
+ "firstSeen": "2026-11-08",
211
+ "note": "Relayed from InPost hours after Yanwen's pickup scan, while the parcel is still in China, with the code 03 where a place goes: the sender's data reaching the last-mile carrier, a notice that keeps the stage before it."
212
+ },
213
+ {
214
+ "wording": "Preference applied",
215
+ "language": "en",
216
+ "confirmedBy": "live",
217
+ "firstSeen": "2026-11-08",
218
+ "note": "Relayed from InPost, with its name where a place goes, before the parcel leaves Yanwen's facility: the recipient's delivery choice, a notice that keeps the stage before it."
219
+ },
206
220
  {
207
221
  "wording": "Shipment information received",
208
222
  "language": "en",
@@ -288,6 +302,14 @@
288
302
  "firstSeen": "2026-10-08",
289
303
  "note": "The last-mile carrier's handover scan; Yanwen's pickup scan is the acceptance."
290
304
  },
305
+ {
306
+ "wording": "Parcel Picked Up",
307
+ "language": "en",
308
+ "stage": "in_transit",
309
+ "confirmedBy": "live",
310
+ "firstSeen": "2026-11-08",
311
+ "note": "Relayed from InPost, with its name where a place goes, before Yanwen's outbound scan: the last-mile carrier collecting the parcel from Yanwen, a handover; Yanwen's pickup scan is the acceptance."
312
+ },
291
313
  {
292
314
  "wording": "Your shipment has arrived at sorting center",
293
315
  "language": "en",
@@ -748,13 +748,13 @@ export function trackingLanguageStage(description) {
748
748
  // Accepted by CTT: its "Aceite" state, and "O envio foi aceite" in its scans. A return request accepted is no parcel.
749
749
  if (/^aceite(?:\.|$)|\b(?:envio|objeto|objecto|encomenda|remessa) foi aceite\b/.test(text))
750
750
  return 'accepted';
751
- // A cross-dock transfer, a flight taking off or landing, a tally in a bonded
751
+ // A cross-dock transfer, a flight taking off, gone or landing, a tally in a bonded
752
752
  // warehouse, a hand-over to the last mile being prepared or to an airline, a
753
753
  // parcel bagged, unbagged or consolidated onto a master air waybill, a parcel
754
754
  // ready for a facility's outbound dispatch or leaving a warehouse, and a
755
755
  // forwarder's inbound scan at a facility, which may follow in brackets, move
756
756
  // the parcel on.
757
- if (/\bcross ?dock(?:ed|ing)?\b|\bready for outbound\b|^(?:(?:item|parcel|package|shipment) )?inbound scan(?:ned|ning)?\b|\b(?:un)?bagg(?:ed|ing)\b|\bmaster (?:air ?way ?bill|awb)(?: (?:number|no))? (?:received|assigned|allocated)\b|\bhand ?over to (?:the |an )?(?:airline|air carrier)\b|^(?:(?:the |your )?(?:item|parcel|package|shipment) )?(?:has )?left (?:the|our) warehouse$|\b(?:flight|leg)\b[^.;]*\b(?:is taking off|took off|has taken off|landed)\b|\bflight (?:take ?off|landing)\b|^(?:initial|final) landing$|\bcargo tally in the supervised warehouse\b|\bprepared for (?:the )?last[- ]mile hand ?over\b/.test(text))
757
+ if (/\bcross ?dock(?:ed|ing)?\b|\bready for outbound\b|^(?:(?:item|parcel|package|shipment) )?inbound scan(?:ned|ning)?\b|\b(?:un)?bagg(?:ed|ing)\b|\bmaster (?:air ?way ?bill|awb)(?: (?:number|no))? (?:received|assigned|allocated)\b|\bhand ?over to (?:the |an )?(?:airline|air carrier)\b|^(?:(?:the |your )?(?:item|parcel|package|shipment) )?(?:has )?left (?:the|our) warehouse$|\b(?:flight|leg)\b[^.;]*\b(?:is taking off|took off|has taken off|has left|landed)\b|\bflight (?:take ?off|landing)\b|^(?:initial|final) landing$|\bcargo tally in the supervised warehouse\b|\bprepared for (?:the )?last[- ]mile hand ?over\b/.test(text))
758
758
  return 'in_transit';
759
759
  // The airline's notice of arrival (NOA) reaching the destination agent, after the landing and
760
760
  // before clearance, as Shouwu, JRT and the ground handlers word it.
@@ -29,6 +29,7 @@ import { DHLINK_RECORD_ZONE, usClockCopy } from '../../carriers/dhlink/clock.js'
29
29
  import { epostGlobalCopyZone } from '../../carriers/epost-global/status.js';
30
30
  import { FIRST_LINE_CHINA_ZONE, firstLineInChina } from '../../carriers/first-line/clock.js';
31
31
  import { hrvatskaPostaForeignZone } from '../../carriers/hrvatska-posta/parser.js';
32
+ import { imileCountryClock } from '../../carriers/imile/clock.js';
32
33
  import { jsExpressChinaClock } from '../../carriers/js-express/clock.js';
33
34
  import { pitneyBowesPlatformClock, pitneyBowesPostalRelay } from '../../carriers/pitney-bowes/clock.js';
34
35
  import { pocztaPolskaPartnerClock } from '../../carriers/poczta-polska/clock.js';
@@ -1033,8 +1034,10 @@ function carrierClock(payload, state, number, status = '', destination = null) {
1033
1034
  return null;
1034
1035
  if (key === 'sunyousytrack' && SUNYOU_DESTINATION_LEG.test(status.trim()))
1035
1036
  return countryTimeZone(destination) ?? undefined;
1036
- if (carrier === 'imile')
1037
- return IMILE_FORWARDER_COPY.test(status.trim()) ? 'Asia/Shanghai' : countryTimeZone(destination);
1037
+ if (carrier === 'imile') {
1038
+ const country = countryCode(destination);
1039
+ return IMILE_FORWARDER_COPY.test(status.trim()) ? 'Asia/Shanghai' : country ? imileCountryClock(country) : null;
1040
+ }
1038
1041
  if (carrier === 'mailamericas' && MAILAMERICAS_DESTINATION_CUSTOMS.test(status.trim())) {
1039
1042
  return MAILAMERICAS_DESTINATION_ZONES[countryCode(destination) ?? ''] ?? undefined;
1040
1043
  }
@@ -23,6 +23,7 @@ import { correosOriginZone } from '../../carriers/correos-spain/clock.js';
23
23
  import { seventeenFeiaCopyZone } from '../../carriers/17feia/clock.js';
24
24
  import { cttSiteZone, portugalZone } from '../../carriers/ctt/clock.js';
25
25
  import { gofoScanZone } from '../../carriers/gofo/clock.js';
26
+ import { imileCountryClock } from '../../carriers/imile/clock.js';
26
27
  import { swissPostDestinationZone } from '../../carriers/swiss-post/clock.js';
27
28
  import { trackingPlace } from '../../places/trackingLocation.js';
28
29
  import { courierZone, universalCarrierHints } from '../shared/hints.js';
@@ -141,6 +142,8 @@ const RELABELLED_FEEDS = {
141
142
  '4px-tracking': { shift: 'Asia/Shanghai' },
142
143
  // A status is 17FEIA's words, then its label or "OTHER".
143
144
  'feia-tracking': { place: (location, _journey, status) => seventeenFeiaCopyZone(status.replace(/,\s*[^,]*$/, ''), location), walls: true },
145
+ // A country of several clocks keeps its wall clocks, as iMile's adapter does, not iMile's Dubai clock.
146
+ 'imile-tracking': { clock: (journey) => journey.destination ? imileCountryClock(journey.destination) : null, walls: true },
144
147
  };
145
148
  function feedOf(scan) {
146
149
  const code = isRecord(scan.courier) ? scan.courier.crawler_code : null;
@@ -378,7 +381,7 @@ export function parseShip24Response(payload, trackingNumber, timezone = null, no
378
381
  const unnamed = feed || (forwarding && isRecord(raw.courier) && raw.courier.crawler_code === 'mainfreight-tracking');
379
382
  // Correos's copy of an origin post's scan takes that post's clock, or keeps its wall clock.
380
383
  const sent = wall ? correosOriginCopy(raw, journey) : undefined;
381
- const placed = !wall ? null : carrierZone(relayed)
384
+ const placed = !wall ? null : carrierZone(relayed) ?? carrierZone(feed?.clock?.(journey))
382
385
  ?? (sent !== undefined ? carrierZone(sent) : placedZone(journey, raw, wall, feed) ?? (unnamed ? null : carrierZone(namedZone(journey, wall))));
383
386
  readings.push({ event: parsed, wall, feed, country: feed?.country ?? locationCountry(raw.location), zone: placed?.zone ?? null, basis: placed?.basis });
384
387
  }
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.1024.0-main.5559"
5
+ "version": "0.1024.2-main.5565"
6
6
  },
7
7
  "paths": {
8
8
  "/health": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-parcel-scraper",
3
- "version": "0.1024.0-main.5559",
3
+ "version": "0.1024.2-main.5565",
4
4
  "description": "Self-hosted parcel tracking: carrier detection, dedicated scrapers and optional universal providers.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",