universal-parcel-scraper 0.330.0-main.1887 → 0.331.0-main.1889

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
@@ -13,7 +13,7 @@ The engine behind [Peek](https://peektracker.com) ([GitHub](https://github.com/p
13
13
  [![License: Apache-2.0](https://img.shields.io/badge/core-Apache--2.0-blue.svg)](LICENSE)
14
14
 
15
15
  <!-- GENERATED:summary -->
16
- **3,500+ carriers** through **368 dedicated adapters** and **5 universal fallbacks**
16
+ **3,500+ carriers** through **369 dedicated adapters** and **5 universal fallbacks**
17
17
 
18
18
  <sub>687 catalog entries · 131 countries represented</sub>
19
19
  <!-- /GENERATED:summary -->
@@ -155,7 +155,7 @@ createTracker({ providers: ['ParcelsApp', 'Ship24', '17TRACK', 'Postal Ninja', '
155
155
  <!-- GENERATED:stages -->
156
156
  <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">
157
157
 
158
- The carrier folders record 13,010 such statuses, each filed under one stage.
158
+ The carrier folders record 13,019 such statuses, each filed under one stage.
159
159
  <!-- /GENERATED:stages -->
160
160
 
161
161
  Wording nobody recorded yet goes through a classifier that reads seven European languages.
package/data/catalog.json CHANGED
@@ -11783,7 +11783,9 @@
11783
11783
  "timezone": "Asia/Shanghai",
11784
11784
  "tracking": {
11785
11785
  "mode": "automatic",
11786
- "adapter": "universal"
11786
+ "adapter": "equick",
11787
+ "recognitionRank": 853,
11788
+ "localClocks": true
11787
11789
  },
11788
11790
  "canaryUrl": "https://api.equick.cn/",
11789
11791
  "linkRules": [
@@ -11795,7 +11797,12 @@
11795
11797
  "fragment": "^/support/supportWay\\?(?:[^#]*&)?nums=([^&,]+)"
11796
11798
  }
11797
11799
  ],
11798
- "detectionRules": []
11800
+ "detectionRules": [
11801
+ {
11802
+ "pattern": "^EQK[A-Z]{2}\\d{10}YQ$",
11803
+ "confidence": "high"
11804
+ }
11805
+ ]
11799
11806
  },
11800
11807
  "estes": {
11801
11808
  "displayName": "Estes",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "catalog": 687,
3
- "activeAdapters": 368,
3
+ "activeAdapters": 369,
4
4
  "countries": 131,
5
5
  "referenceCarriers": 100,
6
6
  "comparison": {
@@ -67090,15 +67090,19 @@
67090
67090
  },
67091
67091
  {
67092
67092
  "input": "EQKPT8586061210YQ",
67093
- "carrier": "unknown",
67094
- "confidence": "none",
67095
- "candidates": []
67093
+ "carrier": "equick",
67094
+ "confidence": "high",
67095
+ "candidates": [
67096
+ "equick"
67097
+ ]
67096
67098
  },
67097
67099
  {
67098
67100
  "input": "EQKPT8613050786YQ",
67099
- "carrier": "unknown",
67100
- "confidence": "none",
67101
- "candidates": []
67101
+ "carrier": "equick",
67102
+ "confidence": "high",
67103
+ "candidates": [
67104
+ "equick"
67105
+ ]
67102
67106
  },
67103
67107
  {
67104
67108
  "input": "ER107045826VN",
@@ -9174,14 +9174,19 @@ var Pe = {
9174
9174
  timezone: "Asia/Shanghai",
9175
9175
  tracking: {
9176
9176
  mode: "automatic",
9177
- adapter: "universal"
9177
+ adapter: "equick",
9178
+ recognitionRank: 853,
9179
+ localClocks: !0
9178
9180
  },
9179
9181
  canaryUrl: "https://api.equick.cn/",
9180
9182
  linkRules: [{
9181
9183
  domains: ["www.equick.cn", "equick.cn"],
9182
9184
  fragment: "^/support/supportWay\\?(?:[^#]*&)?nums=([^&,]+)"
9183
9185
  }],
9184
- detectionRules: []
9186
+ detectionRules: [{
9187
+ pattern: "^EQK[A-Z]{2}\\d{10}YQ$",
9188
+ confidence: "high"
9189
+ }]
9185
9190
  },
9186
9191
  estes: {
9187
9192
  displayName: "Estes",
@@ -21274,7 +21279,7 @@ var Pe = {
21274
21279
  "envia-colombia": [],
21275
21280
  envialia: [],
21276
21281
  "epost-global": ["epost-global-1"],
21277
- equick: [],
21282
+ equick: ["equick-eqk"],
21278
21283
  estes: [],
21279
21284
  "estes-forwarding": [],
21280
21285
  "ethiopian-post": [],
@@ -26933,6 +26938,7 @@ var pl = {
26933
26938
  "envia-colombia": 912,
26934
26939
  envialia: 260,
26935
26940
  "epost-global": 65,
26941
+ equick: 853,
26936
26942
  estes: 20,
26937
26943
  evri: 605,
26938
26944
  "evri-uk": 615,
@@ -0,0 +1,19 @@
1
+ import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
2
+ import type { CarrierResult } from '../../core/result/index.js';
3
+ import { type StepRecorder } from '../../core/telemetry/index.js';
4
+ /**
5
+ * The lookup Equick's tracking page makes for the numbers typed in. The page
6
+ * calls the same service on `api.equick.cn:8080` over plain HTTP; its HTTPS
7
+ * host answers the same.
8
+ */
9
+ export declare const EQUICK_API = "https://api.equick.cn/api/traceQuery";
10
+ export declare class EquickTracker {
11
+ private readonly options;
12
+ constructor(options?: {
13
+ fetcher?: typeof fetch;
14
+ recorder?: StepRecorder;
15
+ userAgent?: string;
16
+ });
17
+ fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
18
+ }
19
+ export declare const adapter: AdapterFactory;
@@ -0,0 +1,65 @@
1
+ import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
2
+ import { ChallengeError, IndeterminateError, SchemaError, TransportError } from '../../core/errors/index.js';
3
+ import { runSteps } from '../../core/runner/index.js';
4
+ import { firstAttemptMs, networkRetryStep } from '../../core/runner/networkRetry.js';
5
+ import { NOOP_RECORDER } from '../../core/telemetry/index.js';
6
+ import { decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
7
+ import { EQUICK_MAX_BYTES, normalizeEquickNumber, parseEquick, PROVIDER } from './parser.js';
8
+ /**
9
+ * The lookup Equick's tracking page makes for the numbers typed in. The page
10
+ * calls the same service on `api.equick.cn:8080` over plain HTTP; its HTTPS
11
+ * host answers the same.
12
+ */
13
+ export const EQUICK_API = 'https://api.equick.cn/api/traceQuery';
14
+ const REDIRECTS = [301, 302, 303, 307, 308];
15
+ // A missing route is not a missing parcel: an unknown number gets an empty result.
16
+ const GONE = [404, 405, 410];
17
+ // Bot checks a CDN or firewall could put in place of the reply.
18
+ const CHALLENGE = /_cf_chl_opt|cf-turnstile|<title>\s*(?:Just a moment|Attention Required|Access Denied|403 Forbidden)|acw_sc__v2|aliyun_waf|_____tmd_____|captcha|<title>\s*(?:滑动验证|验证码|安全验证)/i;
19
+ function json(body) {
20
+ try {
21
+ return JSON.parse(body);
22
+ }
23
+ catch (cause) {
24
+ if (CHALLENGE.test(body.slice(0, 64_000)))
25
+ throw new ChallengeError(PROVIDER, 'Equick answered with a browser check', { cause });
26
+ throw new SchemaError(PROVIDER, 'Equick returned a reply that is not JSON', { cause });
27
+ }
28
+ }
29
+ export class EquickTracker {
30
+ options;
31
+ constructor(options = {}) {
32
+ this.options = options;
33
+ }
34
+ async fetch(raw, context = {}) {
35
+ const number = normalizeEquickNumber(raw);
36
+ const read = async (signal, timeoutMs) => {
37
+ const { response, bytes } = await fetchBounded(EQUICK_API, { method: 'POST', signal, headers: {
38
+ Accept: 'application/json, text/plain, */*', 'Content-Type': 'application/json', 'User-Agent': userAgentOf(this.options.userAgent),
39
+ }, body: JSON.stringify({ txtDanHao: number }) }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)),
40
+ maxBytes: EQUICK_MAX_BYTES, redirect: 'manual', fetcher: this.options.fetcher, allowHttpStatuses: [...REDIRECTS, ...GONE] });
41
+ if (REDIRECTS.includes(response.status))
42
+ throw new TransportError(PROVIDER, 'Equick redirected its tracking lookup');
43
+ if (GONE.includes(response.status))
44
+ throw new TransportError(PROVIDER, 'Equick tracking lookup is unavailable', { status: response.status });
45
+ if (response.status !== 200)
46
+ throw new IndeterminateError(PROVIDER, 'Equick did not answer with its tracking result');
47
+ return parseEquick(json(decodeText(bytes)), number);
48
+ };
49
+ // A read that fails to reach Equick, or hangs, gets one more: the first gets
50
+ // half of the budget, so the retry fits in the rest.
51
+ return runSteps({ carrier: 'equick', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
52
+ recorder: this.options.recorder ?? NOOP_RECORDER }, [
53
+ { id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
54
+ networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
55
+ ]);
56
+ }
57
+ }
58
+ export const adapter = (environment) => {
59
+ const tracker = new EquickTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
60
+ return {
61
+ id: 'equick', recordsSteps: true, steps: ['direct', 'retry'],
62
+ track: (input, context) => tracker.fetch(input.number, context),
63
+ recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeEquickNumber(number))),
64
+ };
65
+ };
@@ -23,16 +23,51 @@
23
23
  "timezone": "Asia/Shanghai",
24
24
  "tracking": {
25
25
  "mode": "automatic",
26
- "adapter": "universal",
27
- "steps": []
26
+ "adapter": "equick",
27
+ "steps": [
28
+ "direct",
29
+ "retry"
30
+ ],
31
+ "recognition": {
32
+ "rank": 853
33
+ },
34
+ "localClocks": true
28
35
  },
29
- "capabilities": [],
36
+ "capabilities": [
37
+ "history",
38
+ "location",
39
+ "destination_country",
40
+ "delivery_tracking_number"
41
+ ],
30
42
  "portal": {
31
43
  "canaryUrl": "https://api.equick.cn/",
32
- "shows": [],
33
- "scraped": [],
44
+ "shows": [
45
+ "status",
46
+ "history",
47
+ "location",
48
+ "destination_country"
49
+ ],
50
+ "scraped": [
51
+ "status",
52
+ "history",
53
+ "location",
54
+ "destination_country",
55
+ "delivery_tracking_number"
56
+ ],
34
57
  "discarded": [],
35
- "unavailable": []
58
+ "unavailable": [
59
+ "eta",
60
+ "delivered_at",
61
+ "offset_timestamps",
62
+ "weight",
63
+ "service_name",
64
+ "delivery_partner",
65
+ "provider_code",
66
+ "origin_country",
67
+ "sender_name",
68
+ "recipient_name",
69
+ "proof_of_delivery"
70
+ ]
36
71
  },
37
72
  "links": [
38
73
  {
@@ -43,5 +78,11 @@
43
78
  "fragment": "^/support/supportWay\\?(?:[^#]*&)?nums=([^&,]+)"
44
79
  }
45
80
  ],
46
- "detection": []
81
+ "detection": [
82
+ {
83
+ "id": "equick-eqk",
84
+ "pattern": "^EQK[A-Z]{2}\\d{10}YQ$",
85
+ "confidence": "high"
86
+ }
87
+ ]
47
88
  }
@@ -53,15 +53,14 @@
53
53
  "dateKind": "capture",
54
54
  "access": "full_page"
55
55
  },
56
- "quarantine": true,
57
56
  "context": {
58
- "assessment": "review"
57
+ "assessment": "confirmed"
59
58
  },
60
59
  "expect": {
61
- "carrier": "unknown",
62
- "confidence": "none"
60
+ "carrier": "equick",
61
+ "confidence": "high"
63
62
  },
64
- "note": "ParcelPanel's Equick China page offers it as the sample number to try; nothing from Equick shows the EQK layout."
63
+ "note": "ParcelPanel's Equick China page offers it as the sample number to try. Equick's own tracker knows it."
65
64
  },
66
65
  {
67
66
  "number": "EQKPT8586061210YQ",
@@ -73,15 +72,14 @@
73
72
  "dateKind": "capture",
74
73
  "access": "full_page"
75
74
  },
76
- "quarantine": true,
77
75
  "context": {
78
- "assessment": "review"
76
+ "assessment": "candidate"
79
77
  },
80
78
  "expect": {
81
- "carrier": "unknown",
82
- "confidence": "none"
79
+ "carrier": "equick",
80
+ "confidence": "high"
83
81
  },
84
- "note": "TrackingMore's Equick China page gives it as a format example; nothing from Equick shows the EQK layout."
82
+ "note": "TrackingMore's Equick China page gives it as a format example, in the layout of the numbers Equick's own tracker knows."
85
83
  }
86
84
  ]
87
85
  }
@@ -0,0 +1,16 @@
1
+ import type { CarrierResult } from '../../core/result/index.js';
2
+ export declare const PROVIDER = "Equick";
3
+ export declare const EQUICK_MAX_BYTES: number;
4
+ /** An Equick number, in capitals; anything else is not one Equick issues. */
5
+ export declare function normalizeEquickNumber(raw: string): string;
6
+ /** A scan's wording without who signed for it or phone numbers. */
7
+ export declare function equickWording(value: string): string;
8
+ /**
9
+ * The reply of the tracker's `POST /api/traceQuery` for one number:
10
+ * `{"code":1,"msg":"success","data":{…}}`, where `data` keys each number asked,
11
+ * as it was sent, to its scans. A number the tracker does not know has no key,
12
+ * so one asked alone gets `{}`, which the page reports as no result. A `code`
13
+ * other than 1 is a lookup the service did not complete. Every scan must name
14
+ * the whole number asked.
15
+ */
16
+ export declare function parseEquick(payload: unknown, raw: string): CarrierResult;
@@ -0,0 +1,151 @@
1
+ import { normalizeTrackingNumber } from '../../core/detection/index.js';
2
+ import { IndeterminateError, InvalidInputError, NoHistoryError, NotFoundError, SchemaError } from '../../core/errors/index.js';
3
+ import { languageStageStatus } from '../../core/status/index.js';
4
+ import { calendarDay, countryCode } from '../../core/time/index.js';
5
+ import { clean } from '../../core/transport/index.js';
6
+ import { isRecord } from '../../core/types.js';
7
+ import { equickScanStage } from './status.js';
8
+ export const PROVIDER = 'Equick';
9
+ // A long history is a few kilobytes; the tracker answers up to fifty numbers a request.
10
+ export const EQUICK_MAX_BYTES = 512 * 1024;
11
+ // EQK, two letters, ten digits and YQ, as the current tracker's numbers read, or the nine
12
+ // digits of the older tracker's.
13
+ const NUMBER = /^(?:EQK[A-Z]{2}\d{10}YQ|\d{9})$/;
14
+ // The delivering carrier's number.
15
+ const PARTNER_NUMBER = /^(?=.*\d)[A-Z0-9]{4,40}$/;
16
+ // "2029-12-14 18:35:22", the scan's wall clock.
17
+ const CLOCK = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})(?::(\d{2}))?$/;
18
+ // A run of nine or more digits, with spaces, brackets, dots or hyphens between them, as phone
19
+ // numbers are written; a date stays.
20
+ const PHONE = /(?<![A-Z\d])(?!\d{4}-\d{2}-\d{2}(?!\d))\+?\d(?:[\s().-]{0,2}\d){8,}(?![A-Z\d])/gi;
21
+ // Who took a delivered parcel, after the delivery's own words.
22
+ const RECEIVER = /\s*[,;:.\-–]?\s*\b(?:signed(?: for)? by|signatory|received by)\b[\s\S]*$/i;
23
+ const MAX_NUMBERS = 50;
24
+ const MAX_ROWS = 500;
25
+ /** An Equick number, in capitals; anything else is not one Equick issues. */
26
+ export function normalizeEquickNumber(raw) {
27
+ const number = raw.length <= 64 ? normalizeTrackingNumber(raw) : '';
28
+ if (!NUMBER.test(number))
29
+ throw new InvalidInputError(PROVIDER, 'Equick numbers are EQK, two letters, ten digits and YQ, or nine digits');
30
+ return number;
31
+ }
32
+ /** An optional text field; anything but a string is a changed reply. */
33
+ function text(row, key, maxLength = 300) {
34
+ const value = row[key];
35
+ if (value === null || value === undefined)
36
+ return '';
37
+ if (typeof value !== 'string')
38
+ throw new SchemaError(PROVIDER, `Equick returned an invalid ${key}`);
39
+ return clean(value, maxLength);
40
+ }
41
+ /** A scan's wording without who signed for it or phone numbers. */
42
+ export function equickWording(value) {
43
+ return clean(value.replace(RECEIVER, '').replace(PHONE, '').replace(/\s+([,;:.])/g, '$1'))
44
+ .replace(/^[\s,;:\-–]+|[\s,;:\-–]+$/g, '');
45
+ }
46
+ /** A scan's clock as an ISO wall time, to the second; the day and time must exist. */
47
+ function wallClock(value) {
48
+ const match = CLOCK.exec(value);
49
+ const date = match ? calendarDay(Number(match[1]), Number(match[2]), Number(match[3])) : null;
50
+ const [hour, minute, second] = [Number(match?.[4]), Number(match?.[5]), Number(match?.[6] ?? 0)];
51
+ if (!date || hour > 23 || minute > 59 || second > 59)
52
+ throw new SchemaError(PROVIDER, 'Equick returned an invalid scan time');
53
+ return `${date}T${match[4]}:${match[5]}:${match[6] ?? '00'}`;
54
+ }
55
+ /** The scans under the number's key, deduplicated, with the delivering carrier's numbers they carry. */
56
+ function scans(rows, number) {
57
+ const seen = new Set();
58
+ const events = [];
59
+ const partners = new Set();
60
+ let country = '';
61
+ for (const row of rows) {
62
+ if (!isRecord(row) || typeof row.traceLabelNo !== 'string')
63
+ throw new SchemaError(PROVIDER, 'Equick returned an invalid scan');
64
+ // Every scan names the shipment it belongs to.
65
+ if (normalizeTrackingNumber(row.traceLabelNo) !== number)
66
+ throw new IndeterminateError(PROVIDER, 'Equick answered for another shipment');
67
+ const said = text(row, 'traceContent', 2000);
68
+ if (!said)
69
+ throw new SchemaError(PROVIDER, 'Equick returned a scan without its wording');
70
+ const local = wallClock(text(row, 'traceDateTime', 40));
71
+ const description = equickWording(said);
72
+ country ||= text(row, 'traceCountry', 80);
73
+ const partner = text(row, 'destinationWBNo', 200);
74
+ if (partner && normalizeTrackingNumber(partner) !== number)
75
+ partners.add(normalizeTrackingNumber(partner));
76
+ if (!description)
77
+ continue;
78
+ const location = text(row, 'traceLocation', 120);
79
+ const key = JSON.stringify([local, description, location]);
80
+ if (seen.has(key))
81
+ continue;
82
+ seen.add(key);
83
+ const staged = equickScanStage(description);
84
+ events.push({
85
+ local_time: local,
86
+ description,
87
+ ...(location ? { location } : {}),
88
+ ...(staged ? { stage: staged.stage, stage_source: staged.source } : {}),
89
+ });
90
+ }
91
+ return { events, country, partners };
92
+ }
93
+ /**
94
+ * The reply of the tracker's `POST /api/traceQuery` for one number:
95
+ * `{"code":1,"msg":"success","data":{…}}`, where `data` keys each number asked,
96
+ * as it was sent, to its scans. A number the tracker does not know has no key,
97
+ * so one asked alone gets `{}`, which the page reports as no result. A `code`
98
+ * other than 1 is a lookup the service did not complete. Every scan must name
99
+ * the whole number asked.
100
+ */
101
+ export function parseEquick(payload, raw) {
102
+ const number = normalizeEquickNumber(raw);
103
+ if (!isRecord(payload) || typeof payload.code !== 'number')
104
+ throw new SchemaError(PROVIDER, 'Equick returned an invalid envelope');
105
+ if (payload.code !== 1)
106
+ throw new IndeterminateError(PROVIDER, 'Equick did not complete the lookup');
107
+ const data = payload.data;
108
+ // The service's answer to a request without a number.
109
+ if (data === null)
110
+ throw new IndeterminateError(PROVIDER, 'Equick returned an empty result');
111
+ if (!isRecord(data))
112
+ throw new SchemaError(PROVIDER, 'Equick returned an invalid result');
113
+ const keys = Object.keys(data);
114
+ if (keys.length > MAX_NUMBERS)
115
+ throw new SchemaError(PROVIDER, 'Equick returned too many results');
116
+ if (!keys.length)
117
+ throw new NotFoundError(PROVIDER);
118
+ const mine = keys.filter((key) => normalizeTrackingNumber(key) === number);
119
+ if (!mine.length)
120
+ throw new IndeterminateError(PROVIDER, 'Equick answered for another number');
121
+ if (mine.length > 1)
122
+ throw new IndeterminateError(PROVIDER, 'Equick returned several results for the number');
123
+ const rows = data[mine[0]];
124
+ if (!Array.isArray(rows))
125
+ throw new SchemaError(PROVIDER, 'Equick returned a shipment with invalid scans');
126
+ if (rows.length > MAX_ROWS)
127
+ throw new SchemaError(PROVIDER, 'Equick returned an oversized history');
128
+ const { events, country, partners } = scans(rows, number);
129
+ if (!events.length)
130
+ throw new NoHistoryError(PROVIDER);
131
+ // Newest first, as the tracker lists them by wall clock; an oldest-first reply is turned round.
132
+ // The clocks can be several places', so the order otherwise stands.
133
+ const newestFirst = events[0].local_time < events.at(-1).local_time ? events.reverse() : events;
134
+ const newest = newestFirst[0];
135
+ const scanned = newestFirst.find((event) => event.stage);
136
+ const stage = scanned?.stage;
137
+ const destination = country ? countryCode(country) : null;
138
+ const [partner] = partners;
139
+ return {
140
+ status: stage ? languageStageStatus(stage) : 'unknown',
141
+ ...(stage ? { current_stage: stage, current_stage_source: scanned.stage_source } : {}),
142
+ last_status_text: newest.description,
143
+ last_update: null,
144
+ last_update_local: newest.local_time,
145
+ expected_delivery: null,
146
+ ...(destination ? { destination_country: destination } : country ? { destination_country_name: country } : {}),
147
+ // The delivering carrier's number, when the scans carry one; the reply does not name the carrier.
148
+ ...(partners.size === 1 && PARTNER_NUMBER.test(partner) ? { delivery_tracking_number: partner } : {}),
149
+ events: newestFirst,
150
+ };
151
+ }
@@ -0,0 +1,11 @@
1
+ import { type ClassifiedWording, type Stage } from '../../core/status/index.js';
2
+ import { type CarrierStatusMap } from '../../core/status/statusMap.js';
3
+ /** The stage Equick's map gives a scan's wording. */
4
+ export declare function equickStage(wording: string): Stage | undefined;
5
+ /** A scan's stage: Equick's map, else the shared wording rules, else none. */
6
+ export declare function equickScanStage(wording: string): ClassifiedWording | null;
7
+ /**
8
+ * Scans come without a code. The tracker's Status column (`In progress`,
9
+ * `Hold`) is not kept: it sets no stage, the wording does.
10
+ */
11
+ export declare const statusMap: CarrierStatusMap;
@@ -0,0 +1,30 @@
1
+ import { classifyWording } from '../../core/status/index.js';
2
+ import { normalizeStatusWording } from '../../core/status/statusMap.js';
3
+ import statuses from './statuses.json' with { type: 'json' };
4
+ const ENTRIES = statuses.entries;
5
+ /** The scan wordings seen on Equick's trackers, in `normalizeStatusWording` form. */
6
+ const WORDINGS = new Map(ENTRIES.filter((entry) => !entry.code && entry.stage)
7
+ .map((entry) => [normalizeStatusWording(entry.wording), entry.stage]));
8
+ // A depot, hub, operation centre or airport named by its three-letter code, which changes with the route.
9
+ const SITE_SCAN = /^(?:arrived at|departed from) [a-z]{3} (?:depot|hub|operation center|airport)$/;
10
+ /** The stage Equick's map gives a scan's wording. */
11
+ export function equickStage(wording) {
12
+ const key = normalizeStatusWording(wording);
13
+ return WORDINGS.get(key) ?? (SITE_SCAN.test(key) ? 'in_transit' : undefined);
14
+ }
15
+ /** A scan's stage: Equick's map, else the shared wording rules, else none. */
16
+ export function equickScanStage(wording) {
17
+ const mapped = equickStage(wording);
18
+ if (mapped)
19
+ return { stage: mapped, source: 'carrier_map' };
20
+ const worded = classifyWording(wording, 'in_transit');
21
+ return worded.source === 'none' ? null : worded;
22
+ }
23
+ /**
24
+ * Scans come without a code. The tracker's Status column (`In progress`,
25
+ * `Hold`) is not kept: it sets no stage, the wording does.
26
+ */
27
+ export const statusMap = {
28
+ stage: (code, wording) => (code === null ? equickStage(wording) : undefined),
29
+ gaps: [],
30
+ };
@@ -1,9 +1,18 @@
1
1
  {
2
2
  "carrier": "equick",
3
3
  "entries": [
4
+ {"code": "In progress", "wording": "In progress", "language": "en", "confirmedBy": "live", "note": "The tracker's Status column on most scans; it sets no stage, the scan's wording does."},
5
+ {"code": "Hold", "wording": "Hold", "language": "en", "confirmedBy": "live", "note": "Seen in the Status column of a pick-up scan the parcel moved on from, so it sets no stage; the scan's wording does."},
6
+ {"wording": "Shipping Label Created", "stage": "registered", "language": "en", "confirmedBy": "live"},
4
7
  {"wording": "Delivered", "stage": "delivered", "language": "en", "confirmedBy": "portal", "note": "Shown in the Status field above the history."},
5
- {"wording": "picked up from shipper", "stage": "accepted", "language": "en", "confirmedBy": "portal", "note": "Pick-up from the sender in China."},
8
+ {"wording": "picked up from shipper", "stage": "accepted", "language": "en", "confirmedBy": "portal", "note": "Pick-up from the sender in China; the current tracker writes it Picked up from shipper."},
6
9
  {"wording": "Accepted by courier", "stage": "accepted", "language": "en", "confirmedBy": "portal"},
10
+ {"wording": "Arrived at SHA Depot", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "Shanghai depot; the code changes with the depot."},
11
+ {"wording": "Departed from Original Depot", "stage": "in_transit", "language": "en", "confirmedBy": "live"},
12
+ {"wording": "Arrived at Equick operation center", "stage": "in_transit", "language": "en", "confirmedBy": "live"},
13
+ {"wording": "Departed from Equick operation center", "stage": "in_transit", "language": "en", "confirmedBy": "live"},
14
+ {"wording": "Arrived at original airport", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "The departure airport in China."},
15
+ {"wording": "Shipment not yet received or processed", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "The destination's delivery carrier has the parcel's data but not the parcel, relayed. Equick took the parcel first, so it does not step back to registered."},
7
16
  {"wording": "Arrived at SHA operation center", "stage": "in_transit", "language": "en", "confirmedBy": "portal", "note": "Shanghai operation centre."},
8
17
  {"wording": "Arrived at BJS hub", "stage": "in_transit", "language": "en", "confirmedBy": "portal", "note": "Beijing hub."},
9
18
  {"wording": "Departed from PVG airport", "stage": "in_transit", "language": "en", "confirmedBy": "portal", "note": "The airport code changes with the route, as in Departed from BJS airport."},
@@ -103,6 +103,7 @@ import { statusMap as energiaTk } from '../../carriers/energia-tk/status.js';
103
103
  import { statusMap as enviaColombia } from '../../carriers/envia-colombia/status.js';
104
104
  import { statusMap as envialia } from '../../carriers/envialia/status.js';
105
105
  import { statusMap as epostGlobal } from '../../carriers/epost-global/status.js';
106
+ import { statusMap as equick } from '../../carriers/equick/status.js';
106
107
  import { statusMap as estes } from '../../carriers/estes/status.js';
107
108
  import { statusMap as eurodis } from '../../carriers/eurodis/status.js';
108
109
  import { statusMap as expressOne } from '../../carriers/express-one/status.js';
@@ -307,7 +308,7 @@ export const STATUS_MAPS = {
307
308
  'danske-fragtmaend': danskeFragtmaend, dao, 'day-ross': dayRoss, dellin, delnext, deppon, 'dhl-express': dhlExpress, 'dhl-freight': dhlFreight, dhlink, dpd, 'dpd-at': dpdAt, 'dpd-be': dpdBe,
308
309
  'dpd-de': dpdDe, 'dpd-fr': dpdFr, 'dpd-ie': dpdIe, 'dpd-nl': dpdNl, 'dpd-pl': dpdPl, 'dpd-pt': dpdPt, 'dpd-za': dpdZa, drop2shop, dsv, dx, dynalogic, 'early-bird': earlyBird, 'easy-mail': easyMail,
309
310
  econt, 'egypt-post': egyptPost, elta, emile, 'ems-vietnam': emsVietnam, 'energia-tk': energiaTk, 'envia-colombia': enviaColombia, envialia,
310
- 'epost-global': epostGlobal, estes, eurodis,
311
+ 'epost-global': epostGlobal, equick, estes, eurodis,
311
312
  'express-one': expressOne, 'fan-courier': fanCourier, 'fedex-pl': fedexPl, fermopoint, 'fg-delivery': fgDelivery, 'flash-express': flashExpress,
312
313
  'flash-express-ph': flashExpressPh,
313
314
  'four-px': fourPx, foxpost, fukuyama, gati, gdex,
@@ -11783,7 +11783,9 @@
11783
11783
  "timezone": "Asia/Shanghai",
11784
11784
  "tracking": {
11785
11785
  "mode": "automatic",
11786
- "adapter": "universal"
11786
+ "adapter": "equick",
11787
+ "recognitionRank": 853,
11788
+ "localClocks": true
11787
11789
  },
11788
11790
  "canaryUrl": "https://api.equick.cn/",
11789
11791
  "linkRules": [
@@ -11795,7 +11797,12 @@
11795
11797
  "fragment": "^/support/supportWay\\?(?:[^#]*&)?nums=([^&,]+)"
11796
11798
  }
11797
11799
  ],
11798
- "detectionRules": []
11800
+ "detectionRules": [
11801
+ {
11802
+ "pattern": "^EQK[A-Z]{2}\\d{10}YQ$",
11803
+ "confidence": "high"
11804
+ }
11805
+ ]
11799
11806
  },
11800
11807
  "estes": {
11801
11808
  "displayName": "Estes",
@@ -7650,14 +7650,19 @@ export declare const CARRIER_CATALOG: {
7650
7650
  readonly timezone: "Asia/Shanghai";
7651
7651
  readonly tracking: {
7652
7652
  readonly mode: "automatic";
7653
- readonly adapter: "universal";
7653
+ readonly adapter: "equick";
7654
+ readonly recognitionRank: 853;
7655
+ readonly localClocks: true;
7654
7656
  };
7655
7657
  readonly canaryUrl: "https://api.equick.cn/";
7656
7658
  readonly linkRules: readonly [{
7657
7659
  readonly domains: readonly ["www.equick.cn", "equick.cn"];
7658
7660
  readonly fragment: "^/support/supportWay\\?(?:[^#]*&)?nums=([^&,]+)";
7659
7661
  }];
7660
- readonly detectionRules: readonly [];
7662
+ readonly detectionRules: readonly [{
7663
+ readonly pattern: "^EQK[A-Z]{2}\\d{10}YQ$";
7664
+ readonly confidence: "high";
7665
+ }];
7661
7666
  };
7662
7667
  readonly estes: {
7663
7668
  readonly displayName: "Estes";
@@ -11784,7 +11784,9 @@ export const CARRIER_CATALOG = {
11784
11784
  "timezone": "Asia/Shanghai",
11785
11785
  "tracking": {
11786
11786
  "mode": "automatic",
11787
- "adapter": "universal"
11787
+ "adapter": "equick",
11788
+ "recognitionRank": 853,
11789
+ "localClocks": true
11788
11790
  },
11789
11791
  "canaryUrl": "https://api.equick.cn/",
11790
11792
  "linkRules": [
@@ -11796,7 +11798,12 @@ export const CARRIER_CATALOG = {
11796
11798
  "fragment": "^/support/supportWay\\?(?:[^#]*&)?nums=([^&,]+)"
11797
11799
  }
11798
11800
  ],
11799
- "detectionRules": []
11801
+ "detectionRules": [
11802
+ {
11803
+ "pattern": "^EQK[A-Z]{2}\\d{10}YQ$",
11804
+ "confidence": "high"
11805
+ }
11806
+ ]
11800
11807
  },
11801
11808
  "estes": {
11802
11809
  "displayName": "Estes",
@@ -28478,7 +28485,7 @@ export const DETECTION_RULE_IDS = {
28478
28485
  "envia-colombia": [],
28479
28486
  "envialia": [],
28480
28487
  "epost-global": ["epost-global-1"],
28481
- "equick": [],
28488
+ "equick": ["equick-eqk"],
28482
28489
  "estes": [],
28483
28490
  "estes-forwarding": [],
28484
28491
  "ethiopian-post": [],
@@ -96,6 +96,7 @@ export const CARRIER_RECOGNITION_RANKS = {
96
96
  "envia-colombia": 912,
97
97
  "envialia": 260,
98
98
  "epost-global": 65,
99
+ "equick": 853,
99
100
  "estes": 20,
100
101
  "evri": 605,
101
102
  "evri-uk": 615,
@@ -124,6 +124,7 @@ import { adapter as energiaTk } from '../carriers/energia-tk/adapter.js';
124
124
  import { adapter as enviaColombia } from '../carriers/envia-colombia/adapter.js';
125
125
  import { adapter as envialia } from '../carriers/envialia/adapter.js';
126
126
  import { adapter as epostGlobal } from '../carriers/epost-global/adapter.js';
127
+ import { adapter as equick } from '../carriers/equick/adapter.js';
127
128
  import { adapter as estafeta } from '../carriers/estafeta/adapter.js';
128
129
  import { adapter as estes } from '../carriers/estes/adapter.js';
129
130
  import { adapter as eurodis } from '../carriers/eurodis/adapter.js';
@@ -494,6 +495,7 @@ export const REGISTRY = {
494
495
  "envia-colombia": enviaColombia,
495
496
  "envialia": envialia,
496
497
  "epost-global": epostGlobal,
498
+ "equick": equick,
497
499
  "estafeta": estafeta,
498
500
  "estes": estes,
499
501
  "eurodis": eurodis,
@@ -947,7 +949,7 @@ export const REGISTRY = {
947
949
  "envia-colombia": "envia-colombia",
948
950
  "envialia": "envialia",
949
951
  "epost-global": "epost-global",
950
- "equick": "universal",
952
+ "equick": "equick",
951
953
  "estafeta": "estafeta",
952
954
  "estes": "estes",
953
955
  "estes-forwarding": "universal",
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.330.0"
5
+ "version": "0.331.0"
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.330.0-main.1887",
3
+ "version": "0.331.0-main.1889",
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",