universal-parcel-scraper 0.969.3-main.5259 → 0.969.4-main.5261

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
@@ -163,7 +163,7 @@ the command line's `track --source ParcelsApp` does the same.
163
163
  <!-- GENERATED:stages -->
164
164
  <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">
165
165
 
166
- The carrier folders record 18,928 such statuses, each filed under one stage.
166
+ The carrier folders record 18,932 such statuses, each filed under one stage.
167
167
  <!-- /GENERATED:stages -->
168
168
 
169
169
  Wording nobody recorded yet goes through a classifier that reads seven European languages.
@@ -14,11 +14,29 @@ export const sameInstantIdentityPolicy = {
14
14
  ...translated,
15
15
  // A row stored with a label that names no place, without a letter as the parser reads it, is
16
16
  // taken over by the row without it. So is one stored at the tracker's "Array", whatever stage
17
- // the map has given its wording since, and an iMile delivery stored with its receiver type.
17
+ // the map has given its wording since, an iMile delivery stored with its receiver type, and
18
+ // China Post's scan from the feed that keeps minutes, less than a minute from the other's.
18
19
  matches: (incoming, stored) => translated.matches(incoming, stored)
19
20
  || matchesDroppedLocation(incoming, stored, (location) => !/\p{L}/u.test(location))
20
- || listPlace(incoming, stored) || receiverCut(incoming, stored),
21
+ || listPlace(incoming, stored) || receiverCut(incoming, stored) || officeCoded(incoming, stored),
22
+ matchWithinMinute: true,
21
23
  };
24
+ // China Post's domestic scans, as the tracker relays them, come from two feeds: one dates them to the
25
+ // minute and places them in the town ("郑州市"), the other to the second, with the office's code after
26
+ // the town ("郑州市 45004400"). The first can word an arrival for the customer, the parcel a 快件 and the
27
+ // centre's slogan after it, where the other has 邮件 and the centre alone.
28
+ const OFFICE_CODE = /^(\S.*?)\s+\d{6,10}$/;
29
+ const POST_SCAN = /^(?:快件|邮件)(到达|离开)(【[^】]{1,80}】)/;
30
+ const postScan = (wording) => POST_SCAN.exec(normalized(wording))?.slice(1).join('');
31
+ function officeCoded(incoming, stored) {
32
+ const [place, saved] = [incoming.location.trim(), stored.location.trim()];
33
+ const [coded, savedCoded] = [OFFICE_CODE.exec(place), OFFICE_CODE.exec(saved)];
34
+ if (!coded === !savedCoded || (coded?.[1] ?? place) !== (savedCoded?.[1] ?? saved))
35
+ return false;
36
+ const scan = postScan(incoming.description);
37
+ return (sameWording(incoming.description, stored.description) || (scan !== undefined && scan === postScan(stored.description)))
38
+ && (!incoming.providerCode || !stored.providerCode || incoming.providerCode === stored.providerCode);
39
+ }
22
40
  function listPlace(incoming, stored) {
23
41
  return stored.location.trim() === LIST_PLACE && !incoming.location.trim() && sameWording(incoming.description, stored.description);
24
42
  }
@@ -1,6 +1,7 @@
1
1
  import { languageStageStatus } from '../../core/status/index.js';
2
2
  import { normalizeStatusWording } from '../../core/status/statusMap.js';
3
3
  import { chinaPostRelayedStage } from '../china-post/status.js';
4
+ import { jAndTAuMappedStage } from '../j-and-t-au/status.js';
4
5
  const WORDING = new Map([
5
6
  ['Delivered.', { status: 'delivered', stage: 'delivered' }],
6
7
  ['Shipment out for delivery.', { status: 'out_for_delivery', stage: 'out_for_delivery' }],
@@ -136,10 +137,18 @@ const DELIVERY_ICON = 'LM40';
136
137
  const withoutPrefix = (description) => description.replace(/^【[^】]*】\s*/, '').replace(/^[a-z]{2}\.(?=\S)/i, '').trim();
137
138
  // A partner's postal event can end with its country's code: "Arrival at Destination Post (Country: TR)".
138
139
  const bare = (description) => withoutPrefix(description).replace(/\s*\(Country(?: code)?: [a-z]{2}\)$/i, '');
140
+ // J&T Express Australia's scans after Yanwen's batch handover at the destination port, relayed with its customer
141
+ // service line: read as J&T's own map reads them, but its pickup is Yanwen's handover, not the parcel's acceptance.
142
+ const HOTLINE = /[\s.]*customer service\s*[::]\s*\d[\d\s]*$/i;
143
+ function jAndTAuRelayedStage(wording) {
144
+ const stage = jAndTAuMappedStage(wording.replace(HOTLINE, '.'));
145
+ return stage === 'accepted' ? 'in_transit' : stage;
146
+ }
139
147
  // China Post's domestic scans, which Yanwen relays in Chinese with the centre in 【】, read as China Post's own
140
- // map reads them: "邮件离开【…】,正在发往下一站" in transit, the security check's "【…】退回" an exception.
148
+ // map reads them: "邮件离开【…】,正在发往下一站" in transit, the security check's "【…】退回" an exception. Then J&T Australia's.
149
+ const relayedStage = (normalized) => chinaPostRelayedStage(normalized) ?? jAndTAuRelayedStage(normalized);
141
150
  function relayedStatus(description) {
142
- const stage = chinaPostRelayedStage(normalizeStatusWording(description));
151
+ const stage = relayedStage(normalizeStatusWording(description));
143
152
  return stage ? { status: languageStageStatus(stage), stage } : undefined;
144
153
  }
145
154
  export function yanwenStatus(description, code) {
@@ -157,7 +166,7 @@ const NOTICES = new Map([
157
166
  export const yanwenNotice = (description) => NOTICES.has(normalizeStatusWording(withoutPrefix(description)));
158
167
  /** What the map says about one scan by its milestone icon, the only code it keeps, else its wording. */
159
168
  export const statusMap = {
160
- stage: (code, wording) => (code === DELIVERY_ICON ? 'delivered' : STAGE_BY_WORDING.get(bare(wording)) ?? chinaPostRelayedStage(wording)),
169
+ stage: (code, wording) => (code === DELIVERY_ICON ? 'delivered' : STAGE_BY_WORDING.get(bare(wording)) ?? relayedStage(wording)),
161
170
  gaps: [
162
171
  { wording: 'ua.', note: "A partner's scan in Ukraine that Yanwen relays as its country's code alone, without words: it names no stage." },
163
172
  ...[...NOTICES].map(([wording, note]) => ({ wording, note })),
@@ -675,6 +675,38 @@
675
675
  "firstSeen": "2026-11-03",
676
676
  "note": "China Post's domestic scan, which Yanwen relays in Chinese with the centre in 【】; read as China Post's map reads it."
677
677
  },
678
+ {
679
+ "wording": "The parcel has been successfully picked up and is being prepared for transportation.",
680
+ "language": "en",
681
+ "stage": "in_transit",
682
+ "confirmedBy": "live",
683
+ "firstSeen": "2026-11-05",
684
+ "note": "J&T Express Australia's pickup from Yanwen's batch handover at the destination port, relayed with J&T's customer service line after it: a handover, not the parcel's acceptance, which J&T's own history reads it as."
685
+ },
686
+ {
687
+ "wording": "The parcel is dispatched from the transit center to the next destination",
688
+ "language": "en",
689
+ "stage": "in_transit",
690
+ "confirmedBy": "live",
691
+ "firstSeen": "2026-11-05",
692
+ "note": "J&T Express Australia's scan, which Yanwen relays after its batch handover at the destination port; read as J&T's map reads it."
693
+ },
694
+ {
695
+ "wording": "The parcel arrives at the distribution center and is prepared for transport to the next destination.",
696
+ "language": "en",
697
+ "stage": "in_transit",
698
+ "confirmedBy": "live",
699
+ "firstSeen": "2026-11-05",
700
+ "note": "J&T Express Australia's scan, which Yanwen relays after its batch handover at the destination port; read as J&T's map reads it."
701
+ },
702
+ {
703
+ "wording": "Your parcel is out for delivery.",
704
+ "language": "en",
705
+ "stage": "out_for_delivery",
706
+ "confirmedBy": "live",
707
+ "firstSeen": "2026-11-05",
708
+ "note": "J&T Express Australia's scan, which Yanwen relays with J&T's courtesy and customer service line after it; read as J&T's map reads it."
709
+ },
678
710
  {
679
711
  "wording": "送交出口海关",
680
712
  "language": "zh",
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.969.3-main.5259"
5
+ "version": "0.969.4-main.5261"
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.969.3-main.5259",
3
+ "version": "0.969.4-main.5261",
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",