universal-parcel-scraper 0.783.0-main.4193 → 0.784.1-main.4205

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
@@ -161,7 +161,7 @@ the command line's `track --source ParcelsApp` does the same.
161
161
  <!-- GENERATED:stages -->
162
162
  <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">
163
163
 
164
- The carrier folders record 17,467 such statuses, each filed under one stage.
164
+ The carrier folders record 17,484 such statuses, each filed under one stage.
165
165
  <!-- /GENERATED:stages -->
166
166
 
167
167
  Wording nobody recorded yet goes through a classifier that reads seven European languages.
package/data/catalog.json CHANGED
@@ -11733,6 +11733,10 @@
11733
11733
  {
11734
11734
  "pattern": "^YF\\d{10}CN$",
11735
11735
  "confidence": "low"
11736
+ },
11737
+ {
11738
+ "pattern": "^KTDWE\\d{10}$",
11739
+ "confidence": "low"
11736
11740
  }
11737
11741
  ]
11738
11742
  },
@@ -19735,7 +19739,7 @@
19735
19739
  "detectionRules": [
19736
19740
  {
19737
19741
  "pattern": "^KTDWE\\d{10}$",
19738
- "confidence": "high"
19742
+ "confidence": "low"
19739
19743
  }
19740
19744
  ]
19741
19745
  },
@@ -80578,12 +80578,28 @@
80578
80578
  "colis-prive"
80579
80579
  ]
80580
80580
  },
80581
+ {
80582
+ "input": "KTDWE0000000001",
80583
+ "carrier": "unknown",
80584
+ "confidence": "low",
80585
+ "candidates": [
80586
+ "dhlink",
80587
+ "ktd"
80588
+ ],
80589
+ "asked": [
80590
+ "dhlink"
80591
+ ]
80592
+ },
80581
80593
  {
80582
80594
  "input": "KTDWE9900000001",
80583
- "carrier": "ktd",
80584
- "confidence": "high",
80595
+ "carrier": "unknown",
80596
+ "confidence": "low",
80585
80597
  "candidates": [
80598
+ "dhlink",
80586
80599
  "ktd"
80600
+ ],
80601
+ "asked": [
80602
+ "dhlink"
80587
80603
  ]
80588
80604
  },
80589
80605
  {
@@ -9254,6 +9254,10 @@ var tt = {
9254
9254
  {
9255
9255
  pattern: "^YF\\d{10}CN$",
9256
9256
  confidence: "low"
9257
+ },
9258
+ {
9259
+ pattern: "^KTDWE\\d{10}$",
9260
+ confidence: "low"
9257
9261
  }
9258
9262
  ]
9259
9263
  },
@@ -15149,7 +15153,7 @@ var tt = {
15149
15153
  linkRules: [],
15150
15154
  detectionRules: [{
15151
15155
  pattern: "^KTDWE\\d{10}$",
15152
- confidence: "high"
15156
+ confidence: "low"
15153
15157
  }]
15154
15158
  },
15155
15159
  "kuehne-nagel": {
@@ -26205,7 +26209,8 @@ var tt = {
26205
26209
  "dhlink-ylus",
26206
26210
  "dhlink-eyt",
26207
26211
  "dhlink-qh",
26208
- "dhlink-yf"
26212
+ "dhlink-yf",
26213
+ "dhlink-ktdwe"
26209
26214
  ],
26210
26215
  "direct-couriers": [],
26211
26216
  "direct-freight-express": [],
@@ -7,11 +7,12 @@ import { calendarDay, countryCode, countryTimeZone, explicitOffsetTime, firstOcc
7
7
  import { clean } from '../../core/transport/index.js';
8
8
  import { xmlDocument } from '../../core/transport/xml.js';
9
9
  import { isChronopostService } from './identity.js';
10
- import { chronopostStage, isChronopostNotification, isPickupDropOff, isReturnToSender } from './status.js';
10
+ import { chronopostStage, isChronopostNotification, isOtherPickupPoint, isPickupDropOff, isReturnToSender } from './status.js';
11
11
  export const CHRONOPOST_MAX_BYTES = 2_000_000;
12
12
  const SOAP = 'http://schemas.xmlsoap.org/soap/envelope/';
13
13
  export const CHRONOPOST_NAMESPACE = 'http://cxf.tracking.soap.chronopost.fr/';
14
14
  const MAX_EVENTS = 500;
15
+ const UNMOVED = new Set(['pending', 'registered', 'accepted']);
15
16
  // The DPD networks that look a Geopost reference up for a parcel Chronopost
16
17
  // sends to their country.
17
18
  const GEOPOST_PARTNERS = new Map([['DE', 'dpd-de'], ['PT', 'dpd-pt']]);
@@ -213,6 +214,7 @@ export function parseChronopostTrackingXml(xml, rawNumber) {
213
214
  return { event, mapped, notification: isChronopostNotification(description), index, redelivery,
214
215
  appointment: start === undefined && end === undefined ? undefined : appointmentWindow(start ?? '', end ?? ''),
215
216
  dropOff: isPickupDropOff(code, description), returnStart: isReturnToSender(code, description),
217
+ otherPoint: isOtherPickupPoint(code, description),
216
218
  // A delivery point is a relay only when the scan names how it is collected.
217
219
  pickup: relay ? relayPoint(relay) : collection && point ? relayPoint(point) : '' };
218
220
  });
@@ -234,7 +236,10 @@ export function parseChronopostTrackingXml(xml, rawNumber) {
234
236
  // Once the parcel starts back to the sender, the scans that follow are the return's,
235
237
  // and a delivery reaches the sender.
236
238
  returning ||= scan.returnStart;
237
- const mapped = returning && scan.mapped.stage === 'delivered' ? { ...scan.mapped, stage: 'returned' } : scan.mapped;
239
+ // Before the parcel has moved, a pickup point's scan of it at another point than the one
240
+ // planned is the sender's drop-off there: the point hands the parcel to the courier next.
241
+ const mapped = scan.otherPoint && UNMOVED.has(stage) ? { stage: 'accepted', source: 'carrier_map' }
242
+ : returning && scan.mapped.stage === 'delivered' ? { ...scan.mapped, stage: 'returned' } : scan.mapped;
238
243
  if (!kept && mapped.source !== 'none') {
239
244
  stage = mapped.stage;
240
245
  stageSource = mapped.source;
@@ -8,6 +8,8 @@ export declare function isChronopostNotification(label: string): boolean;
8
8
  export declare function isReturnToSender(code: string, label: string): boolean;
9
9
  /** The courier's drop-off scan, which can follow the pickup point's own arrival scan. */
10
10
  export declare function isPickupDropOff(code: string, label: string): boolean;
11
+ /** A pickup point's scan of a parcel at another point than the one planned. */
12
+ export declare function isOtherPickupPoint(code: string, label: string): boolean;
11
13
  /** Partner events can reuse codes; the observed wording must agree with the code. */
12
14
  export declare function chronopostStage(code: string, label: string): ClassifiedWording;
13
15
  /** What the map says about one scan: a code whose observed wording it carries. */
@@ -42,6 +42,7 @@ const OBSERVED = [
42
42
  ['RB', 'Colis en cours de livraison au point de retrait', 'in_transit'],
43
43
  ['AB', 'Colis mis à disposition au point de retrait', 'ready_for_pickup'],
44
44
  // The courier left the parcel at another pickup point than the one planned, where it waits.
45
+ // Before the parcel has moved, it is the sender's drop-off at another point (see the parser).
45
46
  ['AB', 'Colis mis à disposition dans un point de retrait différent de celui prévu', 'ready_for_pickup'],
46
47
  // The same arrival at the pickup point, also written under SD and under MD.
47
48
  ['SD', 'Colis mis à disposition au point de retrait', 'ready_for_pickup'],
@@ -78,6 +79,10 @@ export function isReturnToSender(code, label) {
78
79
  export function isPickupDropOff(code, label) {
79
80
  return code === 'RB' && label === 'Colis en cours de livraison au point de retrait';
80
81
  }
82
+ /** A pickup point's scan of a parcel at another point than the one planned. */
83
+ export function isOtherPickupPoint(code, label) {
84
+ return code === 'AB' && label === 'Colis mis à disposition dans un point de retrait différent de celui prévu';
85
+ }
81
86
  /** Partner events can reuse codes; the observed wording must agree with the code. */
82
87
  export function chronopostStage(code, label) {
83
88
  const stage = observed.get(code)?.get(label);
@@ -317,7 +317,7 @@
317
317
  "stage": "ready_for_pickup",
318
318
  "confirmedBy": "live",
319
319
  "firstSeen": "2026-10-27",
320
- "note": "Chronopost trackSkybillV2; code mapping requires matching wording. The parcel waits at another pickup point than the one planned.",
320
+ "note": "Chronopost trackSkybillV2; code mapping requires matching wording. The parcel waits at another pickup point than the one planned. Before the parcel has moved, the parser reads it as the sender's drop-off at another point, accepted.",
321
321
  "wording": "Colis mis à disposition dans un point de retrait différent de celui prévu",
322
322
  "language": "fr"
323
323
  },
@@ -113,6 +113,11 @@
113
113
  "id": "dhlink-yf",
114
114
  "pattern": "^YF\\d{10}CN$",
115
115
  "confidence": "low"
116
+ },
117
+ {
118
+ "id": "dhlink-ktdwe",
119
+ "pattern": "^KTDWE\\d{10}$",
120
+ "confidence": "low"
116
121
  }
117
122
  ]
118
123
  }
@@ -135,6 +135,22 @@
135
135
  ]
136
136
  },
137
137
  "note": "Made-up number in the shape of the YF tracking numbers DHLink's lookup finds as the parcel's tracking number, on a US line GOFO delivers."
138
+ },
139
+ {
140
+ "number": "KTDWE0000000001",
141
+ "role": "shipment",
142
+ "evidence": "synthetic",
143
+ "derivedFrom": "donated_real",
144
+ "shapeConfirmed": "2026-10-28",
145
+ "expect": {
146
+ "carrier": "unknown",
147
+ "confidence": "low",
148
+ "candidates": [
149
+ "dhlink",
150
+ "ktd"
151
+ ]
152
+ },
153
+ "note": "Made-up number in the shape of KTD's tracking numbers DHLink's lookup finds as the parcel's tracking number, on a US line SwiftX delivers."
138
154
  }
139
155
  ]
140
156
  }
@@ -80,6 +80,10 @@ const WORDINGS = new Map([
80
80
  ['departed destination hub', 'in_transit'],
81
81
  // Its last-mile warehouse, then the delivery round.
82
82
  ['arrived at warehouse', 'in_transit'],
83
+ // The flight landed in the destination country.
84
+ ['package arrived at destination country', 'in_transit'],
85
+ // The last-mile partner's depot taking in the parcel the line handed over.
86
+ ['shipment received in depot', 'in_transit'],
83
87
  ['out for delivery', 'out_for_delivery'],
84
88
  // China Post hands the dispatch to the airline after taking the parcel.
85
89
  ['handed over to airlines', 'in_transit'],
@@ -109,8 +113,9 @@ const PATTERNS = [
109
113
  [/^已交承运商运输$/, 'in_transit'],
110
114
  [/已出口直封$/, 'in_transit'],
111
115
  [/^出口海关\/放行$/, 'in_transit'],
112
- // China's export clearance done ("domestic clearance"), as its QG001 node words it in English.
116
+ // China's export clearance done ("domestic clearance"), as its QG001 node words it in English, and its export declaration.
113
117
  [/^国内清关完成$/, 'in_transit'],
118
+ [/^出口申报完成$/, 'in_transit'],
114
119
  // At the airport waiting for a flight, in Chinese with DHLink's English.
115
120
  [/^arriving at the airport - arranging flights\b|^到达机场-安排航班中$/, 'in_transit'],
116
121
  [/^到达寄达地(?:处理中心)?$/, 'in_transit'],
@@ -82,6 +82,9 @@
82
82
  {"wording": "离开【GB】处理中心", "stage": "in_transit", "language": "zh", "confirmedBy": "live", "note": "Left the destination's processing centre."},
83
83
  {"wording": "【GB】安排投递", "stage": "out_for_delivery", "language": "zh", "confirmedBy": "live", "note": "Out for delivery abroad; any country."},
84
84
  {"wording": "【GB】已妥投", "stage": "delivered", "language": "zh", "confirmedBy": "live", "note": "Delivered abroad; any country."},
85
- {"wording": "No Access to Delivery Location", "stage": "failed_attempt", "language": "en", "confirmedBy": "live", "note": "USPS's attempt, relayed after the post office's place and ZIP code; the adapter drops the ZIP code."}
85
+ {"wording": "No Access to Delivery Location", "stage": "failed_attempt", "language": "en", "confirmedBy": "live", "note": "USPS's attempt, relayed after the post office's place and ZIP code; the adapter drops the ZIP code."},
86
+ {"wording": "SHIPMENT RECEIVED IN DEPOT", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "The last-mile partner's depot, after the line handed the parcel over at its destination warehouse."},
87
+ {"wording": "出口申报完成", "stage": "in_transit", "language": "zh", "confirmedBy": "live", "note": "China's export declaration is done, before the flight."},
88
+ {"wording": "Package arrived at destination country", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "The flight landed in the destination country."}
86
89
  ]
87
90
  }
@@ -5,8 +5,12 @@ const WORDINGS = statusWordings(statuses.entries);
5
5
  const FLIGHT_DAY = /^(货物已交机场,预计)\d{1,2}(号的航班 goods delivered to airport, scheduled on flight of the )\d{1,2}(?:st|nd|rd|th)$/;
6
6
  // The English row of a booked flight names its day.
7
7
  const TAKE_OFF_DAY = /^(flight is expected to take off on )[a-z]+ \d{1,2}$/;
8
+ // The newer English rows name the flight's days as month.day: "expected to depart on 10.27 and land on 10.27".
9
+ const FLIGHT_DATES = /^(the flight is expected to depart on )\d{1,2}\.\d{1,2}( and land on )\d{1,2}\.\d{1,2}$/;
10
+ const LANDING_DATE = /^(the flight has departed and is expected to )\d{1,2}\.\d{1,2}( land)$/;
8
11
  /** The key a row's `normalizeStatusWording` wording is mapped under: a booked flight's day becomes an ellipsis. */
9
- export const jrtScanKey = (wording) => wording.replace(FLIGHT_DAY, '$1…$2…').replace(TAKE_OFF_DAY, '$1…');
12
+ export const jrtScanKey = (wording) => wording.replace(FLIGHT_DAY, '$1…$2…').replace(TAKE_OFF_DAY, '$1…')
13
+ .replace(FLIGHT_DATES, '$1…$2…').replace(LANDING_DATE, '$1…$2');
10
14
  /**
11
15
  * The stage JRT's map gives a row, by its whole `normalizeStatusWording` wording:
12
16
  * JRT's own, or the English the adapter shows for it.
@@ -32,6 +32,10 @@
32
32
  {"wording": "Customs clearance of the cargo is in progress", "language": "en", "stage": "customs", "confirmedBy": "live"},
33
33
  {"wording": "Customs clearance completed", "language": "en", "stage": "in_transit", "confirmedBy": "live"},
34
34
  {"wording": "The goods have been handed over to the carrier", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Handed to the carrier that delivers the last leg."},
35
- {"wording": "The shipment has been posted by the sender at the retail outlet", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Copied from DHL: JRT hands the parcel in at a Deutsche Post outlet for the last leg, so the parcel is already on its way."}
35
+ {"wording": "The shipment has been posted by the sender at the retail outlet", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Copied from DHL: JRT hands the parcel in at a Deutsche Post outlet for the last leg, so the parcel is already on its way."},
36
+ {"wording": "The flight is expected to depart on … and land on …", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Waiting at the airport for the booked flight, whose days, month.day, are read as an ellipsis."},
37
+ {"wording": "The flight has departed and is expected to … land", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "In the air; the landing day, month.day, is read as an ellipsis."},
38
+ {"wording": "The flight has arrived and is awaiting dismantling", "language": "en", "stage": "in_transit", "confirmedBy": "live"},
39
+ {"wording": "The NOA has been received", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "The airline's notice of arrival, after the landing and before the pickup and customs clearance."}
36
40
  ]
37
41
  }
@@ -36,7 +36,7 @@
36
36
  {
37
37
  "id": "ktd-1",
38
38
  "pattern": "^KTDWE\\d{10}$",
39
- "confidence": "high"
39
+ "confidence": "low"
40
40
  }
41
41
  ]
42
42
  }
@@ -8,10 +8,14 @@
8
8
  "derivedFrom": "donated_real",
9
9
  "shapeConfirmed": "2026-10-24",
10
10
  "expect": {
11
- "carrier": "ktd",
12
- "confidence": "high"
11
+ "carrier": "unknown",
12
+ "confidence": "low",
13
+ "candidates": [
14
+ "dhlink",
15
+ "ktd"
16
+ ]
13
17
  },
14
- "note": "Made-up number in the shape of the parcels a universal provider files under KTD. No tracker of the carrier confirms it, and no other carrier uses the prefix."
18
+ "note": "Made-up number in the shape of the parcels a universal provider files under KTD. No tracker of the carrier confirms it; DHLink's lookup finds such numbers on its US line SwiftX delivers."
15
19
  }
16
20
  ]
17
21
  }
@@ -63,14 +63,20 @@ const CONTACT = /\s*[((]\s*(?:contact (?:number|phone)|联系电话|聯繫電
63
63
  const COURIER = /\s+by\s+[^()()]{1,40}?(?=\s*[((]\s*(?:contact (?:number|phone)|联系电话|聯繫電話)\s*[::])/iu;
64
64
  const withoutContact = (text) => CONTACT.test(text) ? text.replace(COURIER, ' by the courier').replace(CONTACT, '') : text;
65
65
  // A remark names the site of its scan in its first brackets: "The shipment has arrived at 【A】",
66
- // "Shipment loaded at 【A】, prepare to send to 【B】", or without brackets while it waits in transit:
67
- // "Shipment in transit at AU,TOWN, please wait in patience". A remark about a courier or whoever
68
- // signed may name a person there instead, and a country code given twice, "AU,AU", is no place.
66
+ // "Shipment loaded at 【A】, prepare to send to 【B】". Some name it without them: one waiting in
67
+ // transit, "Shipment in transit at AU,TOWN, please wait in patience", and an operation centre's
68
+ // departure (302) and arrival (305), "The shipment has departed from A ,has been sent to B" and
69
+ // "The shipment has arrived atA". A remark about a courier or whoever signed may name a person
70
+ // there instead, and a country code given twice, "AU,AU", is no place.
69
71
  const PERSON = /courier|deliver(?:y|er) ?(?:man|person|staff)|sign(?:ed|er)|recipient|收派员|快递员|派件员|配送员|签收|簽收|联系|聯繫/iu;
70
72
  const IN_TRANSIT_AT = /\bin transit at\s+([^【】]*?\p{L}[^【】]*?)\s*[,,]\s*please wait/iu;
73
+ const CENTRE = new Map([
74
+ ['302', /\bdeparted from\s*([^【】,,]*?\p{L}[^【】,,]*?)\s*[,,]\s*has been sent to\b/iu],
75
+ ['305', /\barrived at\s*([^【】,,]*?\p{L}[^【】,,]*?)\s*$/iu],
76
+ ]);
71
77
  const COUNTRY_TWICE = /^([A-Z]{2})\s*[,,]\s*\1$/iu;
72
- function scanSite(remark) {
73
- const site = (/【([^【】]*\p{L}[^【】]*)】/u.exec(remark) ?? IN_TRANSIT_AT.exec(remark))?.[1]?.trim();
78
+ function scanSite(remark, code) {
79
+ const site = (/【([^【】]*\p{L}[^【】]*)】/u.exec(remark) ?? IN_TRANSIT_AT.exec(remark) ?? CENTRE.get(code)?.exec(remark))?.[1]?.trim();
74
80
  return site && site.length <= 100 && !COUNTRY_TWICE.test(site) && !PERSON.test(remark) ? site : undefined;
75
81
  }
76
82
  /** Parse the anonymous Taiwan routes endpoint, never a logged-in waybill detail. */
@@ -118,7 +124,7 @@ export function parse(payload, trackingNumber) {
118
124
  throw new SchemaError(PROVIDER, 'SF Express returned an invalid operation code');
119
125
  const status = sfExpressEventStatus(code, reason, text);
120
126
  const description = status?.status === 'delivered' ? 'Delivered' : withoutContact(englishSites(withoutAddresses(text)));
121
- const site = status?.status === 'delivered' ? undefined : scanSite(description);
127
+ const site = status?.status === 'delivered' ? undefined : scanSite(description, code);
122
128
  const event = { time, provider_code: code, description,
123
129
  ...(site ? { location: site } : {}),
124
130
  ...(status ? { stage: status.stage } : {}),
@@ -1,6 +1,6 @@
1
1
  import type { ClassifiedStatus } from '../../core/status/index.js';
2
2
  import type { CarrierStatusMap } from '../../core/status/statusMap.js';
3
- /** Codes 8000 and 70 are ambiguous without their reason, and 99 without its remark; do not default them to signed. */
3
+ /** Codes 8000 and 70 are ambiguous without their reason, 70 and 99 without their remark; do not default them to signed. */
4
4
  export declare function sfExpressEventStatus(code: string, reason: string, remark?: string): ClassifiedStatus | undefined;
5
5
  /**
6
6
  * What the map says about one scan by its operation code, and code 99 by its
@@ -12,6 +12,11 @@ const UNDELIVERED = new Map([
12
12
  ['4', { stage: 'failed_attempt', status: 'exception' }],
13
13
  ['222', { stage: 'ready_for_pickup', status: 'out_for_delivery' }],
14
14
  ]);
15
+ // Without a reason it knows, code 70 is read by its remark: the courier waits at a receiving
16
+ // warehouse that inspects the parcel before it accepts it, the step before the delivery.
17
+ const UNDELIVERED_REMARKS = [
18
+ [/^The shipment has arrived at the warehouse and is waiting for inspection and acceptance\b/i, { stage: 'out_for_delivery', status: 'out_for_delivery' }],
19
+ ];
15
20
  const SUMMARY = new Map([['2', EXCEPTION], ['3', TRANSIT], ['4', RETURNED], ['5', EXCEPTION], ['6', EXCEPTION], ['99', TRANSIT], ['100', DELIVERED]]);
16
21
  // The official Taiwan frontend's operation-code groups, plus codes observed in
17
22
  // its public route response whose remarks settle them. The warehouse
@@ -35,13 +40,13 @@ const NOTICES = [
35
40
  [/^The shipment is being returned\.?$/i, RETURNED],
36
41
  [/^The shipment is being forwarded\.?$/i, TRANSIT],
37
42
  ];
38
- /** Codes 8000 and 70 are ambiguous without their reason, and 99 without its remark; do not default them to signed. */
43
+ /** Codes 8000 and 70 are ambiguous without their reason, 70 and 99 without their remark; do not default them to signed. */
39
44
  export function sfExpressEventStatus(code, reason, remark = '') {
40
45
  if (code === '8000') {
41
46
  return REASONS.get(reason);
42
47
  }
43
48
  if (code === '70')
44
- return UNDELIVERED.get(reason);
49
+ return UNDELIVERED.get(reason) ?? UNDELIVERED_REMARKS.find(([wording]) => wording.test(remark.trim()))?.[1];
45
50
  if (code === '99')
46
51
  return NOTICES.find(([wording]) => wording.test(remark.trim()))?.[1];
47
52
  return CODES.get(code);
@@ -466,6 +466,15 @@
466
466
  "confirmedBy": "live",
467
467
  "firstSeen": "2026-10-20",
468
468
  "note": "Official Taiwan routes opCode 99 with this remark: the parcel is sent on to another address."
469
+ },
470
+ {
471
+ "code": "70",
472
+ "wording": "The shipment has arrived at the warehouse and is waiting for inspection and acceptance. Please wait patiently.",
473
+ "stage": "out_for_delivery",
474
+ "language": "en",
475
+ "confirmedBy": "live",
476
+ "firstSeen": "2026-10-27",
477
+ "note": "Official Taiwan routes opCode 70 with this remark: the courier waits at a receiving warehouse that inspects the parcel before it accepts it, then signs."
469
478
  }
470
479
  ]
471
480
  }
@@ -7,6 +7,10 @@
7
7
  {"code": "15", "wording": "Departed from Airport to destination country", "language": "en", "stage": "in_transit", "confirmedBy": "live"},
8
8
  {"code": "15", "wording": "Arrived at destination airport", "language": "en", "stage": "in_transit", "confirmedBy": "live"},
9
9
  {"code": "20", "wording": "Departed from customs", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Released by the destination country's customs. The tracker leaves the country after \"of\" empty, which the adapter drops."},
10
- {"code": "15", "wording": "At U.S. Postal Service facility, Accepted by U.S. Postal Service", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "USPS takes the parcel over from TopYou's line in the United States, after the flight; it is no first acceptance."}
10
+ {"code": "15", "wording": "At U.S. Postal Service facility, Accepted by U.S. Postal Service", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "USPS takes the parcel over from TopYou's line in the United States, after the flight; it is no first acceptance."},
11
+ {"code": "5", "wording": "Carrier received the shipment", "language": "en", "stage": "accepted", "confirmedBy": "live", "note": "The first scan in TopYou's hands, worded for its newer waybills."},
12
+ {"code": "10", "wording": "Shipment arrived at a carrier facility", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Under the code for leaving the first-mile warehouse, after TopYou received the parcel: movement, not its acceptance."},
13
+ {"code": "15", "wording": "Shipment started the customs clearance process at origin", "language": "en", "stage": "customs", "confirmedBy": "live", "note": "Export clearance before the flight."},
14
+ {"code": "20", "wording": "Shipment arrived in the destination country", "language": "en", "stage": "in_transit", "confirmedBy": "live"}
11
15
  ]
12
16
  }
@@ -27,6 +27,12 @@ const ENTRIES = [
27
27
  ['Departed from facility', { status: 'in_transit', stage: 'in_transit' }],
28
28
  // The hand-over to the destination's last-mile carrier, not to the recipient.
29
29
  ['Delivered to local carrier', { status: 'in_transit', stage: 'in_transit' }],
30
+ // The sorting before import customs, and the last-mile carrier's steps it relays up to the delivery round.
31
+ ['Processing at sort facility', { status: 'in_transit', stage: 'in_transit' }],
32
+ ['Arrived at destination', { status: 'in_transit', stage: 'in_transit' }],
33
+ ['Handled by local carrier', { status: 'in_transit', stage: 'in_transit' }],
34
+ ['Transport to delivery centre', { status: 'in_transit', stage: 'in_transit' }],
35
+ ['Arrived at delivery centre', { status: 'in_transit', stage: 'in_transit' }],
30
36
  ['Arrived at sorting center', { status: 'in_transit', stage: 'in_transit' }],
31
37
  ['The package left sorting center', { status: 'in_transit', stage: 'in_transit' }],
32
38
  ['Arrived at GOFO Regional Destination Facility', { status: 'in_transit', stage: 'in_transit' }],
@@ -229,6 +229,45 @@
229
229
  "firstSeen": "2026-10-08",
230
230
  "note": "The handover to the last-mile carrier, not a delivery to the recipient."
231
231
  },
232
+ {
233
+ "wording": "Processing at sort facility",
234
+ "stage": "in_transit",
235
+ "language": "en",
236
+ "confirmedBy": "live",
237
+ "firstSeen": "2026-10-28",
238
+ "note": "The destination sorting, before import customs and again after the hand-over."
239
+ },
240
+ {
241
+ "wording": "Arrived at destination",
242
+ "stage": "in_transit",
243
+ "language": "en",
244
+ "confirmedBy": "live",
245
+ "firstSeen": "2026-10-28",
246
+ "note": "The last-mile carrier's arrival in the country, relayed after the hand-over; not a delivery."
247
+ },
248
+ {
249
+ "wording": "Handled by local carrier",
250
+ "stage": "in_transit",
251
+ "language": "en",
252
+ "confirmedBy": "live",
253
+ "firstSeen": "2026-10-28",
254
+ "note": "The last-mile carrier's transit, relayed."
255
+ },
256
+ {
257
+ "wording": "Transport to delivery centre",
258
+ "stage": "in_transit",
259
+ "language": "en",
260
+ "confirmedBy": "live",
261
+ "firstSeen": "2026-10-28"
262
+ },
263
+ {
264
+ "wording": "Arrived at delivery centre",
265
+ "stage": "in_transit",
266
+ "language": "en",
267
+ "confirmedBy": "live",
268
+ "firstSeen": "2026-10-28",
269
+ "note": "The last-mile carrier's local sorting site, before the delivery round."
270
+ },
232
271
  {
233
272
  "wording": "Departure from the international sorting center",
234
273
  "stage": "in_transit",
@@ -11733,6 +11733,10 @@
11733
11733
  {
11734
11734
  "pattern": "^YF\\d{10}CN$",
11735
11735
  "confidence": "low"
11736
+ },
11737
+ {
11738
+ "pattern": "^KTDWE\\d{10}$",
11739
+ "confidence": "low"
11736
11740
  }
11737
11741
  ]
11738
11742
  },
@@ -19735,7 +19739,7 @@
19735
19739
  "detectionRules": [
19736
19740
  {
19737
19741
  "pattern": "^KTDWE\\d{10}$",
19738
- "confidence": "high"
19742
+ "confidence": "low"
19739
19743
  }
19740
19744
  ]
19741
19745
  },
@@ -7628,6 +7628,9 @@ export declare const CARRIER_CATALOG: {
7628
7628
  }, {
7629
7629
  readonly pattern: "^YF\\d{10}CN$";
7630
7630
  readonly confidence: "low";
7631
+ }, {
7632
+ readonly pattern: "^KTDWE\\d{10}$";
7633
+ readonly confidence: "low";
7631
7634
  }];
7632
7635
  };
7633
7636
  readonly "direct-couriers": {
@@ -12713,7 +12716,7 @@ export declare const CARRIER_CATALOG: {
12713
12716
  readonly linkRules: readonly [];
12714
12717
  readonly detectionRules: readonly [{
12715
12718
  readonly pattern: "^KTDWE\\d{10}$";
12716
- readonly confidence: "high";
12719
+ readonly confidence: "low";
12717
12720
  }];
12718
12721
  };
12719
12722
  readonly "kuehne-nagel": {
@@ -11734,6 +11734,10 @@ export const CARRIER_CATALOG = {
11734
11734
  {
11735
11735
  "pattern": "^YF\\d{10}CN$",
11736
11736
  "confidence": "low"
11737
+ },
11738
+ {
11739
+ "pattern": "^KTDWE\\d{10}$",
11740
+ "confidence": "low"
11737
11741
  }
11738
11742
  ]
11739
11743
  },
@@ -19736,7 +19740,7 @@ export const CARRIER_CATALOG = {
19736
19740
  "detectionRules": [
19737
19741
  {
19738
19742
  "pattern": "^KTDWE\\d{10}$",
19739
- "confidence": "high"
19743
+ "confidence": "low"
19740
19744
  }
19741
19745
  ]
19742
19746
  },
@@ -35017,7 +35021,7 @@ export const DETECTION_RULE_IDS = {
35017
35021
  "dhl-ecommerce-uk": ["dhl-ecommerce-uk-2", "dhl-ecommerce-uk-1"],
35018
35022
  "dhl-express": ["dhl-express-waybill", "dhl-express-piece", "dhl-express-piece-label", "dhl-express-piece-label-after"],
35019
35023
  "dhl-freight": ["dhl-freight-2"],
35020
- "dhlink": ["dhlink-1", "dhlink-xzg", "dhlink-ylus", "dhlink-eyt", "dhlink-qh", "dhlink-yf"],
35024
+ "dhlink": ["dhlink-1", "dhlink-xzg", "dhlink-ylus", "dhlink-eyt", "dhlink-qh", "dhlink-yf", "dhlink-ktdwe"],
35021
35025
  "direct-couriers": [],
35022
35026
  "direct-freight-express": [],
35023
35027
  "direx": [],
@@ -387,7 +387,9 @@ function speedafZone(payload, date, destination) {
387
387
  * its sites by airport code, and a site listed here gives its zone: its EWR
388
388
  * hub's digits, read on Eastern time, have to the second the instants of
389
389
  * DHLink's copies of the same scans, which DHLink dates on China's clock, and
390
- * its New Jersey warehouse's those SwiftX's own tracker gives. A delivery or
390
+ * its New Jersey warehouse's those SwiftX's own tracker gives. Its ORD hub,
391
+ * at Chicago O'Hare, is on Central time: KTD files its scans at the same
392
+ * digits under the hub's town and state. A delivery or
391
393
  * failed delivery names the recipient's town and no site ("US, EXAMPLE TOWN"):
392
394
  * SwiftX's tracker gives it the offset of the warehouse that sent the parcel
393
395
  * out, so it takes the zone of the newest listed site scanned before it.
@@ -395,7 +397,9 @@ function speedafZone(payload, date, destination) {
395
397
  * Created", at "US, SYSTEM, SYSTEM") carry GMT-07:00 in every season in its
396
398
  * own tracker, and keep that clock wherever they are.
397
399
  */
398
- const SWIFTX_SITES = new Map([['EWR HUB', 'America/New_York'], ['EWR_NJ WAREHOUSE', 'America/New_York']]);
400
+ const SWIFTX_SITES = new Map([
401
+ ['EWR HUB', 'America/New_York'], ['EWR_NJ WAREHOUSE', 'America/New_York'], ['ORD HUB', 'America/Chicago'],
402
+ ]);
399
403
  const SWIFTX_LABEL = /^shipping label created$/i;
400
404
  const SWIFTX_LABEL_CLOCK = 'UTC-7';
401
405
  function swiftxListedZone(location) {
@@ -869,9 +873,11 @@ function brandNetworkZones(payload, name, number, destination, origin) {
869
873
  * that of the US state or Canadian province its place names for certain
870
874
  * ("New York, United States"), else the single clock of the country it
871
875
  * names. One with no place, only a town or a country of several clocks keeps
872
- * its wall clock.
876
+ * its wall clock. KTD's dates do this too: it files SwiftX's hub scans at
877
+ * SwiftX's digits under the hub's town and state, and its flight's take-off
878
+ * in Shanghai and landing in Chicago, which China's clock put 75 minutes apart.
873
879
  */
874
- const PLACE_CLOCK_LINES = new Set(['js-express', 'ecms-express']);
880
+ const PLACE_CLOCK_LINES = new Set(['js-express', 'ecms-express', 'ktd']);
875
881
  function placeClockLine(carrier, status, location) {
876
882
  const china = carrier === 'js-express' ? jsExpressChinaClock(status, location) : undefined;
877
883
  if (china !== undefined)
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.783.0-main.4193"
5
+ "version": "0.784.1-main.4205"
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.783.0-main.4193",
3
+ "version": "0.784.1-main.4205",
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",