universal-parcel-scraper 0.49.1-main.549 → 0.49.2-main.550

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
@@ -149,7 +149,7 @@ createTracker({ providers: ['ParcelsApp', 'Ship24', '17TRACK', 'Postal Ninja', '
149
149
  <!-- GENERATED:stages -->
150
150
  <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">
151
151
 
152
- The carrier folders record 2,756 such statuses, each filed under one stage.
152
+ The carrier folders record 2,758 such statuses, each filed under one stage.
153
153
  <!-- /GENERATED:stages -->
154
154
 
155
155
  Wording nobody recorded yet goes through a classifier that reads seven European languages.
@@ -4937,8 +4937,8 @@ function Yt(e) {
4937
4937
  if (/will be transported|sera (?:transporte|achemine)|wird.*transportiert|sara trasportat/.test(t) || /will be available|sera disponible|(?:sera|va etre|doit etre) (?:distribue|remis|mis en (?:livraison|distribution))|wird.*verfugbar|sara disponibile|to be delivered|a livrer|noch zuzustellen|da consegnare|(?:once|when|as soon as) (?:the |your )?(?:shipment|parcel|package|item) is out for delivery/.test(t) || /\bestara disponi(?:ble|vel)\b|\bpor entregar\b|\bpendiente de (?:entrega|entregar|reparto)\b|\bpendente de entrega\b|\b(?:aguarda|aguardando|a aguardar) (?:a )?entrega\b|\bsera (?:enviad|transportad|expedid|encaminhad)[oa]s?\b/.test(t)) return "in_transit";
4938
4938
  if (/ready for (?:pickup|collection)|ready to collect|available for (?:pickup|collection)|awaiting (?:customer |recipient |your )?(?:pickup|collection)|pret.*(?:retrait|retire)|disponible.*(?:retrait|collecte)|vous attend (?:au|en|dans|chez|a)\b|mise? a disposition (?:au|en|dans|a)\b.*(?:point (?:de )?retrait|relais|consigne|locker|bureau de poste|agence)|jours? restants? pour (?:le )?retirer|abholbereit|zur abholung bereit|pront[oa] per il ritiro|disponibile per il ritiro|(?:deposited|depose|hinterlegt|depositat[oa]).*mypost24/.test(t) || /(?<!\b(?:no|nao|ya no|ja nao) (?:(?:esta|estara|se encuentra|se encontra|e|era|foi) )?)\bdisponi(?:ble|vel) (?:para (?:su |ser )?(?:recoger|recogida|recogid[oa]|retirar|retiro|retirada|retirad[oa]|levantamento|levantar|levantad[oa]|recolha|recolher)|(?:en|no|na|num|numa|em) (?:el |la |un |una |su |tu |nuestr[oa] )?(?:punto|oficina|tienda|locker|taquilla|consigna|citypaq|parcel ?shop|buzon inteligente|agencia|delegacion|sucursal|ponto|loja|cacifo|estacao|balcao|payshop|posto|unidade))\b|\blist[oa] para (?:su |ser )?(?:recoger|recogida|recogid[oa]|retirar|retiro)\b|\bpront[oa] (?:para|a) (?:ser )?(?:levantamento|levantar|levantad[oa]|recolha|recolher|retirada|retirar)\b|\b(?:ya )?(?:puede|puedes|podra|podras) (?:pasar a )?recoger(?:lo|la)?\b|\b(?:aguarda|aguardando|a aguardar) (?:o |a )?(?:levantamento|retirada)\b|\besperando (?:su|tu) recogida\b|\b(?:pendiente de|esperando|a la espera de) (?:ser )?recogid[oa] por (?:el |la )?destinatari[oa]\b/.test(t)) return "ready_for_pickup";
4939
4939
  if (/label (?:created|printed)|(?:created|generated) a (?:new )?(?:shipment |shipping )?label|etiquette.*(?:cree|imprime)|cree une etiquette|versandetikett.*(?:erstellt|gedruckt)|etikett erstellt|etichetta.*(?:creata|stampata)|creato un'etichetta|elektronisch|electroni(?:c|que|sch)|elettronic|pre.?advi[cs]|preannunciat|vorangemeldet|preannonce|data.*entered|donnees.*saisies|daten.*erfasst|dati.*inseriti/.test(t) || /shipment information (?:received|sent to)|\bdata received\b|(?:package|parcel|shipment) data (?:was |has been )?sent to|\bpre ?notification\b|informations d'expedition recues|sendungsinformationen erhalten|informazioni.*spedizione ricevute|(?:consignment|shipment|parcel|package) recorded by|envoi enregistre par|sendung.*absender.*erfasst|spedizione registrata dal/.test(t) || /preparation chez.*expediteur|preparation.*expediteur|preparazione.*mittente|being prepared.*sender|(?:shipper|sender)(?: that)? (?:they are|is|are) preparing|beim absender.*vorbereitet|warehouse of the sender|shippers warehouse|entrepot de l'expediteur|lager des absenders|magazzino del mittente|demande d'envoi.*prise en compte|collection request.*(?:received|recorded)|abholauftrag.*erfasst|richiesta.*ritiro.*registrata/.test(t) || /^(?:reported|recorded|announced|item created|enregistre|annonce|erfasst|angekundigt|registrato|annunciato)$/.test(t)) return "registered";
4940
- if (/attend d'etre libere|attente de liberation|wartet auf.*freigabe|attesa di svincolo|(?:customs|clearance).*\bnot\b.*(?:clear|complet|releas)|dedouanement.*pas termine|customs (?:not cleared|clearance not completed)|not.*released by customs|dedouanement.*(?:non termine|pas termine)|(?:\bnot|n't|\bnever)(?: yet)?(?: been)? cleared (?:through |by )?customs|\bbe(?:ing)? cleared (?:through |by )?customs|non dedouane|zollabfertigung.*nicht abgeschlossen|nicht.*zoll.*freigegeben|sdoganamento.*non completato|non sdoganat/.test(t)) return "customs";
4941
- if (/clearance.*(?:has been completed|completed)|clearance (?:complete|success)|(?:leaving|departed from|left) customs|completion of customs|formalites.*(?:terminee|achevee)|released by (?:customs|a government agency)|customs (?:cleared|(?:has |have )?released)|\bcleared (?:through |by )?customs|dedouanement.*(?:termine|acheve)|fin du dedouanement|libere.*(?:douane|autorite)|douane.*libere|zollabfertigung.*abgeschlossen|(?:zoll|behorde).*freigegeben|sdoganamento.*completato|completamento.*sdoganamento|svincolat.*(?:dogana|autorita)/.test(t)) return "in_transit";
4940
+ if (/attend d'etre libere|attente de liberation|wartet auf.*freigabe|attesa di svincolo|(?:customs|clearance).*\bnot\b.*(?:clear|complet|releas)|dedouanement.*pas termine|customs (?:not cleared|clearance not completed)|not.*released by customs|(?:\bnot|n't|\bnever)(?: yet)?(?: been)? released (?:by |from )?(?:(?:import|export)(?:\/export)? )?customs|\b(?:be|being) released from (?:(?:import|export)(?:\/export)? )?customs|dedouanement.*(?:non termine|pas termine)|(?:\bnot|n't|\bnever)(?: yet)?(?: been)? cleared (?:through |by )?customs|\bbe(?:ing)? cleared (?:through |by )?customs|non dedouane|zollabfertigung.*nicht abgeschlossen|nicht.*zoll.*freigegeben|sdoganamento.*non completato|non sdoganat/.test(t)) return "customs";
4941
+ if (/clearance.*(?:has been completed|completed)|clearance (?:complete|success)|(?:leaving|departed from|left) customs|completion of customs|formalites.*(?:terminee|achevee)|released from (?:(?:import|export)(?:\/export)? )?customs|released by (?:customs|a government agency)|customs (?:cleared|(?:has |have )?released)|\bcleared (?:through |by )?customs|dedouanement.*(?:termine|acheve)|fin du dedouanement|libere.*(?:douane|autorite)|douane.*libere|zollabfertigung.*abgeschlossen|(?:zoll|behorde).*freigegeben|sdoganamento.*completato|completamento.*sdoganamento|svincolat.*(?:dogana|autorita)/.test(t)) return "in_transit";
4942
4942
  if (/customs|clearance|douan|formalites (?:d')?(?:import|export)|zoll|dogan|government agency|autorite gouvernementale|staatliche behorde|autorita governativa/.test(t)) return "customs";
4943
4943
  if (/out for (?:physical )?delivery|being delivered|in delivery|(?:loading|loaded).*delivery vehicle|on (?:\w+ )?vehicle for delivery|(?:courier|driver|delivery champion) has (?:the|your) (?:shipment|parcel|package|item)|en cours de livraison|en livraison|de la livraison de (?:son|votre) colis ce jour|charg(?:e|ement).*vehicule de livraison|in zustellung|zustellfahrzeug.*(?:geladen|verladen)|(?:beladen|verladung).*zustellfahrzeug|in consegna|caric(?:at|amento).*veicolo.*consegna/.test(t) || /^(?:en )?reparto$|\ben reparto\b|\b(?:siendo|sendo|a ser) (?:entregad[oa]s?|entregues?|repartid[oa]s?|distribuid[oa]s?)\b|\b(?:salida|salio|sale|ha salido) (?:a|para) (?:el )?reparto\b|\bruta de (?:reparto|entrega)\b|\ben proceso de entrega\b|\bvehiculo de reparto\b|\b(?:repartidor|mensajero|cartero|conductor) (?:ya )?(?:tiene|lleva) (?:tu|su|el|la) (?:paquete|envio|pedido|encomienda)\b|\bem distribuicao\b|\bsaiu para (?:a )?(?:entrega|distribuicao)\b|\bem (?:rota|processo|curso) de entrega\b|\bem entrega\b|\bveiculo de (?:entrega|distribuicao)\b|\b(?:carteiro|estafeta|motorista|entregador) (?:ja )?(?:tem|leva) (?:a|o) (?:sua |tua )?(?:encomenda|envio|objeto|pacote|volume)\b/.test(t)) return "out_for_delivery";
4944
4944
  if (/\bdelivered\b|^final delivery$|delivery complete(?:d)?\b|delivery (?:was )?successful|(?:a ete|est) distribue|^(?:livre|livree)(?:$|[ ,])|(?:colis|envoi|est|a ete) livre|zugestellt|\bconsegnat[oa]\b|livraison effectuee|consegna completata/.test(t) || !/\b(?:una vez|cuando|hasta que|antes de que|para que|en cuanto|tan pronto como|uma vez|quando|assim que|ate que|logo que)\b[^.;:]*\bentreg/.test(t) && /(?<!\b(?:sea|sean|fuera|fuese|ser|seja|sejam|fosse|for|forem|que|se) )\b(?:entregad[oa]s?|entregues?)\b|\bentrega (?:realizada|efectuada|efetuada|completada|concluida|finalizada)\b|\brecogid[oa] por (?:el |la )?destinatari[oa]\b|\b(?:levantad|retirad)[oa] pelo destinatario\b/.test(t)) return "delivered";
@@ -19,7 +19,7 @@ import { NotFoundError, SchemaError } from '../../core/errors/index.js';
19
19
  import { explicitOffsetTime } from '../../core/time/index.js';
20
20
  import { fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
21
21
  import { isRecord } from '../../core/types.js';
22
- import { CAINIAO_ACTION_STATUS, CAINIAO_STATUS, cainiaoActionCode, cainiaoStageByStatus, } from './status.js';
22
+ import { CAINIAO_ACTION_STATUS, CAINIAO_STATUS, cainiaoActionCode, cainiaoActionStage, cainiaoStageByStatus, } from './status.js';
23
23
  const PROVIDER = 'Cainiao';
24
24
  const UPSTREAM = 'Cainiao tracking';
25
25
  const DETAIL_URL = 'https://global.cainiao.com/global/detail.json';
@@ -86,14 +86,17 @@ function carrierNames(trackingModule, scans) {
86
86
  * Cainiao has no place field: it writes a scan's town in front of its standard
87
87
  * wording, "[Bordeaux] Out for delivery", in every language. A bracket that
88
88
  * reads as a place name becomes the location and the wording keeps the rest.
89
- * Codes, capitals-only names and the module's own carrier names stay in the text.
89
+ * Codes, ambiguous capitals-only names and the module's carrier names stay in the text.
90
90
  */
91
91
  function placedWording(wording, carriers) {
92
92
  const match = /^\s*\[([^[\]]{1,80})\]\s*(\S[\s\S]*)$/.exec(wording);
93
93
  const place = match?.[1].trim() ?? '';
94
+ // An explicit numbered city district is a place even in uppercase. Keep
95
+ // arbitrary numbers and facility codes excluded from the general rule.
96
+ const arrondissement = /^(?:Paris (?:[1-9]|1\d|20)|Lyon [1-9]|Marseille (?:[1-9]|1[0-6]))(?:er|e|eme|ème) arrondissement$/i.test(place);
94
97
  const letters = place.match(/\p{L}/gu)?.length ?? 0;
95
- if (!match || letters < 3 || !/^\p{L}[\p{L}\p{M} .'’-]*$/u.test(place)
96
- || (/\p{Lu}/u.test(place) && !/\p{Ll}/u.test(place)) || carriers.has(place.toLocaleLowerCase('en-US'))) {
98
+ if (!match || letters < 3 || (!arrondissement && (!/^\p{L}[\p{L}\p{M} .'’-]*$/u.test(place)
99
+ || (/\p{Lu}/u.test(place) && !/\p{Ll}/u.test(place)))) || carriers.has(place.toLocaleLowerCase('en-US'))) {
97
100
  return { description: wording, location: '' };
98
101
  }
99
102
  return { description: match[2].trim(), location: place };
@@ -160,11 +163,11 @@ export function parseCainiaoTrackingResponse(value, trackingNumber) {
160
163
  : (CAINIAO_STATUS.get(rawStatus)
161
164
  ?? (rawStatus ? 'in_transit' : details.length === 0 ? 'pending' : 'unknown'));
162
165
  const latestAction = cainiaoActionCode(latest.actionCode);
163
- const actionStage = cainiaoStageByStatus(latestAction);
166
+ const currentStage = cainiaoActionStage(latestAction) ?? cainiaoStageByStatus()[status];
164
167
  const carriers = carrierNames(trackingModule, details);
165
168
  const events = details.slice(0, MAX_EVENTS_TO_RETURN).map((event) => {
166
169
  const code = cainiaoActionCode(event.actionCode);
167
- const mapped = code ? CAINIAO_ACTION_STATUS.get(code) : undefined;
170
+ const stage = cainiaoActionStage(code);
168
171
  const { description, location } = placedWording(text(event.standerdDesc) || text(event.desc), carriers);
169
172
  return {
170
173
  time: scanTime(event),
@@ -172,7 +175,7 @@ export function parseCainiaoTrackingResponse(value, trackingNumber) {
172
175
  description,
173
176
  // The code keys the app's review of a scan the map does not stage.
174
177
  ...(/^[A-Z0-9_]{1,64}$/.test(code) ? { provider_code: code } : {}),
175
- ...(mapped ? { stage: actionStage[mapped] ?? 'in_transit' } : {}),
178
+ ...(stage ? { stage } : {}),
176
179
  };
177
180
  });
178
181
  const eta = record(trackingModule.globalEtaInfo);
@@ -187,7 +190,7 @@ export function parseCainiaoTrackingResponse(value, trackingNumber) {
187
190
  const deliveredAt = status === 'delivered' ? scanTime(latest) || null : null;
188
191
  return {
189
192
  status,
190
- ...(actionStage[status] || status === 'in_transit' ? { current_stage: actionStage[status] ?? 'in_transit' } : {}),
193
+ ...(currentStage ? { current_stage: currentStage } : {}),
191
194
  last_status_text: placedWording(text(latest.standerdDesc) || text(latest.desc), carriers).description || rawStatus,
192
195
  last_update: scanTime(latest) || null,
193
196
  expected_delivery: status === 'delivered' ? null : expected,
@@ -195,7 +198,7 @@ export function parseCainiaoTrackingResponse(value, trackingNumber) {
195
198
  ? {}
196
199
  : { expected_delivery_from: expectedFrom }),
197
200
  ...(deliveredAt ? { delivered_at: deliveredAt } : {}),
198
- ...(handoff ? { delivery_tracking_number: handoff } : {}),
201
+ ...(handoff && comparableIdentifier(handoff) !== requested ? { delivery_tracking_number: handoff } : {}),
199
202
  ...(destination ? { destination_country_name: destination } : {}),
200
203
  events,
201
204
  };
@@ -1,3 +1,4 @@
1
+ import { cainiaoActionCode, cainiaoActionStage } from './status.js';
1
2
  const normalized = (value) => value.replace(/\s+/g, ' ').trim().toLocaleLowerCase('en-US');
2
3
  // A scan stored while its town was still part of the wording, "[Bordeaux] Out for
3
4
  // delivery" without a location, is the same scan as "Out for delivery" in Bordeaux.
@@ -7,8 +8,22 @@ export const sameInstantIdentityPolicy = {
7
8
  matches(incoming, stored) {
8
9
  const wording = normalized(incoming.description);
9
10
  const location = normalized(incoming.location);
10
- return Boolean(wording && location) && !['', 'unknown'].includes(incoming.stage)
11
- && incoming.stage === stored.stage && !normalized(stored.location)
11
+ const movedLocation = Boolean(location) && !normalized(stored.location)
12
12
  && normalized(stored.description) === `[${location}] ${wording}`;
13
+ if (!wording || ['', 'unknown'].includes(incoming.stage))
14
+ return false;
15
+ if (incoming.stage === stored.stage)
16
+ return movedLocation;
17
+ // Correct only the old adapter's coarse stages, with the same action,
18
+ // wording and place. The consumer also requires a unique same-time match.
19
+ const code = cainiaoActionCode(incoming.providerCode);
20
+ if (!code || code !== cainiaoActionCode(stored.providerCode)
21
+ || incoming.stage !== cainiaoActionStage(code))
22
+ return false;
23
+ const correction = (['customs', 'accepted'].includes(incoming.stage) && stored.stage === 'in_transit')
24
+ || (incoming.stage === 'ready_for_pickup' && stored.stage === 'out_for_delivery')
25
+ || (incoming.stage === 'out_for_delivery' && stored.stage === 'ready_for_pickup');
26
+ return correction && (movedLocation || (wording === normalized(stored.description)
27
+ && location === normalized(stored.location)));
13
28
  },
14
29
  };
@@ -24,15 +24,9 @@ export declare const CAINIAO_ACTION_STATUS: Map<string, CarrierStatus>;
24
24
  export declare const CAINIAO_PICKUP_ACTIONS: Set<string>;
25
25
  /** Normalize an action code: Cainiao has been seen spelling them with spaces and in lower case. */
26
26
  export declare function cainiaoActionCode(value: unknown): string;
27
- /**
28
- * Status → stage for one lookup. The pickup distinction depends on the newest
29
- * action code, so the whole table is derived from it once and then applied to
30
- * the shipment and to every historical event, exactly as before the move.
31
- */
32
- export declare function cainiaoStageByStatus(latestActionCode: string): Partial<Record<CarrierStatus, Stage>>;
33
- /**
34
- * What the map says about one scan, by its action code. An out-for-delivery
35
- * code reads as the van or a pickup point by the newest code of the whole
36
- * reply, so the code alone does not answer it.
37
- */
27
+ /** Coarse status fallback when Cainiao supplies no action code. */
28
+ export declare function cainiaoStageByStatus(): Partial<Record<CarrierStatus, Stage>>;
29
+ /** Each scan keeps its own milestone even after the parcel progresses. */
30
+ export declare function cainiaoActionStage(code: string): Stage | undefined;
31
+ /** The review queue uses the same per-scan mapping as the adapter. */
38
32
  export declare const statusMap: CarrierStatusMap;
@@ -51,6 +51,8 @@ export const CAINIAO_ACTION_STATUS = new Map([
51
51
  ['CC_IM_FAILURE', 'exception'],
52
52
  ['CC_IM_EXCEPTION', 'exception'],
53
53
  ['GTMS_ACCEPT', 'in_transit'],
54
+ ['GTMS_DO_ARRIVE', 'in_transit'],
55
+ ['GTMS_STATION_OUT', 'in_transit'],
54
56
  ['GTMS_SC_DEPART', 'in_transit'],
55
57
  ['SC_ARRIVE', 'in_transit'],
56
58
  ['SC_DEPART', 'in_transit'],
@@ -71,19 +73,27 @@ export const CAINIAO_PICKUP_ACTIONS = new Set(['GSTA_INFORM_BUYER', 'GTMS_WAIT_S
71
73
  export function cainiaoActionCode(value) {
72
74
  return (typeof value === 'string' ? value : '').trim().toLocaleUpperCase('en-US').replace(/\s+/g, '_');
73
75
  }
74
- /**
75
- * Status → stage for one lookup. The pickup distinction depends on the newest
76
- * action code, so the whole table is derived from it once and then applied to
77
- * the shipment and to every historical event, exactly as before the move.
78
- */
79
- export function cainiaoStageByStatus(latestActionCode) {
76
+ /** Coarse status fallback when Cainiao supplies no action code. */
77
+ export function cainiaoStageByStatus() {
80
78
  return {
81
79
  pending: 'registered',
82
- out_for_delivery: CAINIAO_PICKUP_ACTIONS.has(latestActionCode) ? 'ready_for_pickup' : 'out_for_delivery',
80
+ in_transit: 'in_transit',
81
+ out_for_delivery: 'out_for_delivery',
83
82
  delivered: 'delivered',
84
83
  exception: 'exception',
85
84
  };
86
85
  }
86
+ /** Each scan keeps its own milestone even after the parcel progresses. */
87
+ export function cainiaoActionStage(code) {
88
+ if (CAINIAO_PICKUP_ACTIONS.has(code))
89
+ return 'ready_for_pickup';
90
+ if (['CC_EX_START', 'CC_IM_START', 'CC_HO_IN_SUCCESS'].includes(code))
91
+ return 'customs';
92
+ if (code === 'PU_PICKUP_SUCCESS')
93
+ return 'accepted';
94
+ const status = CAINIAO_ACTION_STATUS.get(code);
95
+ return status ? cainiaoStageByStatus()[status] : undefined;
96
+ }
87
97
  /**
88
98
  * The scans the review queue holds without their action code, from before
89
99
  * events kept it, by `normalizeStatusWording` wording without the town
@@ -95,18 +105,11 @@ const UNCODED_SCANS = new Map([
95
105
  ['awaiting for transit to final delivery office', 'TD_TRANS_ARRIVE_DCP'],
96
106
  ['departed from destination country/region sorting center', 'GTMS_SC_DEPART'],
97
107
  ]);
98
- /**
99
- * What the map says about one scan, by its action code. An out-for-delivery
100
- * code reads as the van or a pickup point by the newest code of the whole
101
- * reply, so the code alone does not answer it.
102
- */
108
+ /** The review queue uses the same per-scan mapping as the adapter. */
103
109
  export const statusMap = {
104
110
  stage: (code, wording) => {
105
111
  const action = code ? cainiaoActionCode(code) : UNCODED_SCANS.get(wording.replace(/^\[[^\]]*\]\s*/, ''));
106
- const status = action ? CAINIAO_ACTION_STATUS.get(action) : undefined;
107
- if (!status || status === 'out_for_delivery')
108
- return undefined;
109
- return cainiaoStageByStatus('')[status] ?? 'in_transit';
112
+ return action ? cainiaoActionStage(action) : undefined;
110
113
  },
111
114
  gaps: [],
112
115
  };
@@ -101,10 +101,12 @@
101
101
  },
102
102
  {
103
103
  "code": "PU_PICKUP_SUCCESS",
104
- "stage": "in_transit",
105
- "confirmedBy": "prior-art",
104
+ "stage": "accepted",
105
+ "confirmedBy": "live",
106
106
  "firstSeen": "2026-09-12",
107
- "note": ""
107
+ "note": "",
108
+ "wording": "Received by logistics company",
109
+ "language": "en"
108
110
  },
109
111
  {
110
112
  "code": "GWMS_OUTBOUND",
@@ -143,10 +145,12 @@
143
145
  },
144
146
  {
145
147
  "code": "CC_EX_START",
146
- "stage": "in_transit",
147
- "confirmedBy": "prior-art",
148
+ "stage": "customs",
149
+ "confirmedBy": "live",
148
150
  "firstSeen": "2026-09-12",
149
- "note": ""
151
+ "note": "",
152
+ "wording": "Export customs clearance started",
153
+ "language": "en"
150
154
  },
151
155
  {
152
156
  "code": "CC_EX_SUCCESS",
@@ -254,17 +258,21 @@
254
258
  },
255
259
  {
256
260
  "code": "CC_HO_IN_SUCCESS",
257
- "stage": "in_transit",
258
- "confirmedBy": "prior-art",
261
+ "stage": "customs",
262
+ "confirmedBy": "live",
259
263
  "firstSeen": "2026-09-12",
260
- "note": ""
264
+ "note": "",
265
+ "wording": "Arrived at customs",
266
+ "language": "en"
261
267
  },
262
268
  {
263
269
  "code": "CC_IM_START",
264
- "stage": "in_transit",
265
- "confirmedBy": "prior-art",
270
+ "stage": "customs",
271
+ "confirmedBy": "live",
266
272
  "firstSeen": "2026-09-12",
267
- "note": ""
273
+ "note": "",
274
+ "wording": "Import customs clearance started",
275
+ "language": "en"
268
276
  },
269
277
  {
270
278
  "code": "CC_IM_SUCCESS",
@@ -386,6 +394,24 @@
386
394
  "confirmedBy": "prior-art",
387
395
  "firstSeen": "2026-09-12",
388
396
  "note": "Reclassified to exception 2026-09-12."
397
+ },
398
+ {
399
+ "code": "GTMS_DO_ARRIVE",
400
+ "wording": "Arrived at local delivery center",
401
+ "language": "en",
402
+ "stage": "in_transit",
403
+ "confirmedBy": "live",
404
+ "firstSeen": "2026-10-08",
405
+ "note": ""
406
+ },
407
+ {
408
+ "code": "GTMS_STATION_OUT",
409
+ "wording": "Delivery in process. Now left facility.",
410
+ "language": "en",
411
+ "stage": "in_transit",
412
+ "confirmedBy": "live",
413
+ "firstSeen": "2026-10-08",
414
+ "note": ""
389
415
  }
390
416
  ]
391
417
  }
@@ -136,9 +136,9 @@ export function trackingLanguageStage(description) {
136
136
  if (/^(?:reported|recorded|announced|item created|enregistre|annonce|erfasst|angekundigt|registrato|annunciato)$/.test(text))
137
137
  return 'registered';
138
138
  // Completion/release is distinct from pending or explicitly negated clearance.
139
- if (/attend d'etre libere|attente de liberation|wartet auf.*freigabe|attesa di svincolo|(?:customs|clearance).*\bnot\b.*(?:clear|complet|releas)|dedouanement.*pas termine|customs (?:not cleared|clearance not completed)|not.*released by customs|dedouanement.*(?:non termine|pas termine)|(?:\bnot|n't|\bnever)(?: yet)?(?: been)? cleared (?:through |by )?customs|\bbe(?:ing)? cleared (?:through |by )?customs|non dedouane|zollabfertigung.*nicht abgeschlossen|nicht.*zoll.*freigegeben|sdoganamento.*non completato|non sdoganat/.test(text))
139
+ if (/attend d'etre libere|attente de liberation|wartet auf.*freigabe|attesa di svincolo|(?:customs|clearance).*\bnot\b.*(?:clear|complet|releas)|dedouanement.*pas termine|customs (?:not cleared|clearance not completed)|not.*released by customs|(?:\bnot|n't|\bnever)(?: yet)?(?: been)? released (?:by |from )?(?:(?:import|export)(?:\/export)? )?customs|\b(?:be|being) released from (?:(?:import|export)(?:\/export)? )?customs|dedouanement.*(?:non termine|pas termine)|(?:\bnot|n't|\bnever)(?: yet)?(?: been)? cleared (?:through |by )?customs|\bbe(?:ing)? cleared (?:through |by )?customs|non dedouane|zollabfertigung.*nicht abgeschlossen|nicht.*zoll.*freigegeben|sdoganamento.*non completato|non sdoganat/.test(text))
140
140
  return 'customs';
141
- if (/clearance.*(?:has been completed|completed)|clearance (?:complete|success)|(?:leaving|departed from|left) customs|completion of customs|formalites.*(?:terminee|achevee)|released by (?:customs|a government agency)|customs (?:cleared|(?:has |have )?released)|\bcleared (?:through |by )?customs|dedouanement.*(?:termine|acheve)|fin du dedouanement|libere.*(?:douane|autorite)|douane.*libere|zollabfertigung.*abgeschlossen|(?:zoll|behorde).*freigegeben|sdoganamento.*completato|completamento.*sdoganamento|svincolat.*(?:dogana|autorita)/.test(text))
141
+ if (/clearance.*(?:has been completed|completed)|clearance (?:complete|success)|(?:leaving|departed from|left) customs|completion of customs|formalites.*(?:terminee|achevee)|released from (?:(?:import|export)(?:\/export)? )?customs|released by (?:customs|a government agency)|customs (?:cleared|(?:has |have )?released)|\bcleared (?:through |by )?customs|dedouanement.*(?:termine|acheve)|fin du dedouanement|libere.*(?:douane|autorite)|douane.*libere|zollabfertigung.*abgeschlossen|(?:zoll|behorde).*freigegeben|sdoganamento.*completato|completamento.*sdoganamento|svincolat.*(?:dogana|autorita)/.test(text))
142
142
  return 'in_transit';
143
143
  if (/customs|clearance|douan|formalites (?:d')?(?:import|export)|zoll|dogan|government agency|autorite gouvernementale|staatliche behorde|autorita governativa/.test(text))
144
144
  return 'customs';
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.49.1"
5
+ "version": "0.49.2"
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.49.1-main.549",
3
+ "version": "0.49.2-main.550",
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",