universal-parcel-scraper 0.49.0-main.543 → 0.49.0-main.548
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/NOTICE +6 -0
- package/README.md +1 -1
- package/data/detection-golden.json +1 -1
- package/dist/carriers/aliexpress/adapter.js +2 -0
- package/dist/carriers/aliexpress/status.d.ts +7 -0
- package/dist/carriers/aliexpress/status.js +33 -0
- package/dist/carriers/aliexpress/statuses.json +36 -0
- package/dist/carriers/correos-spain/adapter.js +2 -0
- package/dist/carriers/correos-spain/status.d.ts +3 -0
- package/dist/carriers/correos-spain/status.js +16 -0
- package/dist/carriers/dpd/status.js +1 -0
- package/dist/carriers/dpd/statuses.json +8 -0
- package/dist/carriers/dpd-fr/status.d.ts +7 -0
- package/dist/carriers/dpd-fr/status.js +13 -1
- package/dist/carriers/hermes/numbers.json +2 -2
- package/dist/carriers/ups/adapter.js +3 -2
- package/dist/carriers/ups/status.d.ts +4 -2
- package/dist/carriers/ups/status.js +18 -2
- package/dist/carriers/ups/statuses.json +4 -4
- package/dist/core/catalog/statusMaps.js +5 -2
- package/dist/places/hubs.json +2 -1
- package/dist/places/index.js +58 -8
- package/dist/places/pincodes.json +6554 -0
- package/dist/places/places.tsv.br +0 -0
- package/package.json +1 -1
package/NOTICE
CHANGED
|
@@ -14,3 +14,9 @@ Facility coordinates include OpenStreetMap data (https://www.openstreetmap.org/c
|
|
|
14
14
|
licensed under the Open Database License. Natural Earth country label points
|
|
15
15
|
(https://www.naturalearthdata.com) and OurAirports airports
|
|
16
16
|
(https://ourairports.com/data/) are public domain.
|
|
17
|
+
|
|
18
|
+
Indian PIN code towns (places/pincodes.json) are derived from: Department of
|
|
19
|
+
Posts, Ministry of Communications, 2025, All India Pincode Directory till last
|
|
20
|
+
month, Open Government Data Platform India, 26/06,
|
|
21
|
+
https://www.data.gov.in/resource/all-india-pincode-directory-till-last-month.
|
|
22
|
+
Published under Government Open Data License - India: https://www.data.gov.in/Godl.
|
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,
|
|
152
|
+
The carrier folders record 2,756 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.
|
|
@@ -170,6 +170,8 @@ export function parseCainiaoTrackingResponse(value, trackingNumber) {
|
|
|
170
170
|
time: scanTime(event),
|
|
171
171
|
location,
|
|
172
172
|
description,
|
|
173
|
+
// The code keys the app's review of a scan the map does not stage.
|
|
174
|
+
...(/^[A-Z0-9_]{1,64}$/.test(code) ? { provider_code: code } : {}),
|
|
173
175
|
...(mapped ? { stage: actionStage[mapped] ?? 'in_transit' } : {}),
|
|
174
176
|
};
|
|
175
177
|
});
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { CarrierStatus } from '../../core/result/index.js';
|
|
18
18
|
import type { Stage } from '../../core/status/index.js';
|
|
19
|
+
import type { CarrierStatusMap } from '../../core/status/statusMap.js';
|
|
19
20
|
/** Parcel-level order tokens, used only when the newest trace has no action code. */
|
|
20
21
|
export declare const CAINIAO_STATUS: Map<string, CarrierStatus>;
|
|
21
22
|
export declare const CAINIAO_ACTION_STATUS: Map<string, CarrierStatus>;
|
|
@@ -29,3 +30,9 @@ export declare function cainiaoActionCode(value: unknown): string;
|
|
|
29
30
|
* the shipment and to every historical event, exactly as before the move.
|
|
30
31
|
*/
|
|
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
|
+
*/
|
|
38
|
+
export declare const statusMap: CarrierStatusMap;
|
|
@@ -12,6 +12,9 @@ export const CAINIAO_STATUS = new Map([
|
|
|
12
12
|
// leg). The parcel-level `status` token vocabulary is unestablished (real
|
|
13
13
|
// parcels carry DELIVERED/CLEAR_CUSTOMS/transport/pickup/delivered), so the
|
|
14
14
|
// newest latestTrace.actionCode wins and the token is only a fallback.
|
|
15
|
+
// LH_HO_OUT_SUCCESS, TD_TRANSWH_OUTBOUND, TD_TRANS_ARRIVE_DCP and
|
|
16
|
+
// GTMS_SC_DEPART were seen live on 2026-10-08, between the linehaul's arrival
|
|
17
|
+
// and the local partner's round, each worded as one more leg of the journey.
|
|
15
18
|
export const CAINIAO_ACTION_STATUS = new Map([
|
|
16
19
|
['GWMS_ACCEPT', 'pending'],
|
|
17
20
|
['GWMS_PACKAGE', 'pending'],
|
|
@@ -32,12 +35,15 @@ export const CAINIAO_ACTION_STATUS = new Map([
|
|
|
32
35
|
['LH_HO_AIRLINE', 'in_transit'],
|
|
33
36
|
['LH_DEPART', 'in_transit'],
|
|
34
37
|
['LH_ARRIVE', 'in_transit'],
|
|
38
|
+
['LH_HO_OUT_SUCCESS', 'in_transit'],
|
|
35
39
|
['LH_POST_COLLECTION', 'in_transit'],
|
|
36
40
|
['COMMON_INTRANSIT', 'in_transit'],
|
|
37
41
|
['TD_TRANS_DEPART', 'in_transit'],
|
|
38
42
|
['TD_TRANS_ARRIVE', 'in_transit'],
|
|
39
43
|
['TD_TRANS_DEPART_C', 'in_transit'],
|
|
40
44
|
['TD_TRANS_ARRIVE_C', 'in_transit'],
|
|
45
|
+
['TD_TRANSWH_OUTBOUND', 'in_transit'],
|
|
46
|
+
['TD_TRANS_ARRIVE_DCP', 'in_transit'],
|
|
41
47
|
['CC_HO_IN_SUCCESS', 'in_transit'],
|
|
42
48
|
['CC_IM_START', 'in_transit'],
|
|
43
49
|
['CC_IM_SUCCESS', 'in_transit'],
|
|
@@ -45,6 +51,7 @@ export const CAINIAO_ACTION_STATUS = new Map([
|
|
|
45
51
|
['CC_IM_FAILURE', 'exception'],
|
|
46
52
|
['CC_IM_EXCEPTION', 'exception'],
|
|
47
53
|
['GTMS_ACCEPT', 'in_transit'],
|
|
54
|
+
['GTMS_SC_DEPART', 'in_transit'],
|
|
48
55
|
['SC_ARRIVE', 'in_transit'],
|
|
49
56
|
['SC_DEPART', 'in_transit'],
|
|
50
57
|
['OE_DEPART', 'in_transit'],
|
|
@@ -77,3 +84,29 @@ export function cainiaoStageByStatus(latestActionCode) {
|
|
|
77
84
|
exception: 'exception',
|
|
78
85
|
};
|
|
79
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The scans the review queue holds without their action code, from before
|
|
89
|
+
* events kept it, by `normalizeStatusWording` wording without the town
|
|
90
|
+
* bracket. Each is the standard wording Cainiao gives that code.
|
|
91
|
+
*/
|
|
92
|
+
const UNCODED_SCANS = new Map([
|
|
93
|
+
['handed over from linehaul office', 'LH_HO_OUT_SUCCESS'],
|
|
94
|
+
['leaving transit country/region', 'TD_TRANSWH_OUTBOUND'],
|
|
95
|
+
['awaiting for transit to final delivery office', 'TD_TRANS_ARRIVE_DCP'],
|
|
96
|
+
['departed from destination country/region sorting center', 'GTMS_SC_DEPART'],
|
|
97
|
+
]);
|
|
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
|
+
*/
|
|
103
|
+
export const statusMap = {
|
|
104
|
+
stage: (code, wording) => {
|
|
105
|
+
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';
|
|
110
|
+
},
|
|
111
|
+
gaps: [],
|
|
112
|
+
};
|
|
@@ -183,6 +183,15 @@
|
|
|
183
183
|
"firstSeen": "2026-09-12",
|
|
184
184
|
"note": ""
|
|
185
185
|
},
|
|
186
|
+
{
|
|
187
|
+
"code": "LH_HO_OUT_SUCCESS",
|
|
188
|
+
"wording": "Handed over from linehaul office",
|
|
189
|
+
"language": "en",
|
|
190
|
+
"stage": "in_transit",
|
|
191
|
+
"confirmedBy": "live",
|
|
192
|
+
"firstSeen": "2026-10-08",
|
|
193
|
+
"note": "Seen live after the linehaul office arrival, before the transit country scans."
|
|
194
|
+
},
|
|
186
195
|
{
|
|
187
196
|
"code": "LH_POST_COLLECTION",
|
|
188
197
|
"stage": "in_transit",
|
|
@@ -225,6 +234,24 @@
|
|
|
225
234
|
"firstSeen": "2026-09-12",
|
|
226
235
|
"note": ""
|
|
227
236
|
},
|
|
237
|
+
{
|
|
238
|
+
"code": "TD_TRANSWH_OUTBOUND",
|
|
239
|
+
"wording": "Leaving transit country/region",
|
|
240
|
+
"language": "en",
|
|
241
|
+
"stage": "in_transit",
|
|
242
|
+
"confirmedBy": "live",
|
|
243
|
+
"firstSeen": "2026-10-08",
|
|
244
|
+
"note": "Seen live between the arrival in and the departure from the transit country."
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"code": "TD_TRANS_ARRIVE_DCP",
|
|
248
|
+
"wording": "Awaiting for transit to final delivery office",
|
|
249
|
+
"language": "en",
|
|
250
|
+
"stage": "in_transit",
|
|
251
|
+
"confirmedBy": "live",
|
|
252
|
+
"firstSeen": "2026-10-08",
|
|
253
|
+
"note": "Seen live after customs or the transit country, before the local partner received the parcel."
|
|
254
|
+
},
|
|
228
255
|
{
|
|
229
256
|
"code": "CC_HO_IN_SUCCESS",
|
|
230
257
|
"stage": "in_transit",
|
|
@@ -274,6 +301,15 @@
|
|
|
274
301
|
"firstSeen": "2026-09-12",
|
|
275
302
|
"note": ""
|
|
276
303
|
},
|
|
304
|
+
{
|
|
305
|
+
"code": "GTMS_SC_DEPART",
|
|
306
|
+
"wording": "Departed from destination country/region sorting center",
|
|
307
|
+
"language": "en",
|
|
308
|
+
"stage": "in_transit",
|
|
309
|
+
"confirmedBy": "live",
|
|
310
|
+
"firstSeen": "2026-10-08",
|
|
311
|
+
"note": "Seen live between the local partner receiving the parcel and its round."
|
|
312
|
+
},
|
|
277
313
|
{
|
|
278
314
|
"code": "SC_ARRIVE",
|
|
279
315
|
"stage": "in_transit",
|
|
@@ -122,6 +122,8 @@ export function parseCorreosSpainTrackingResponse(payload, trackingNumber, exped
|
|
|
122
122
|
time: time.iso,
|
|
123
123
|
location: '',
|
|
124
124
|
description,
|
|
125
|
+
// The code keys the app's review of a scan the map does not stage.
|
|
126
|
+
...(/^[A-Z0-9.]{1,32}$/.test(code) ? { provider_code: code } : {}),
|
|
125
127
|
...(classified ? { stage: classified.stage } : {}),
|
|
126
128
|
},
|
|
127
129
|
classified,
|
|
@@ -13,5 +13,8 @@
|
|
|
13
13
|
* out for delivery → failed attempt → office hold → collected).
|
|
14
14
|
*/
|
|
15
15
|
import type { ClassifiedStatus } from '../../core/status/index.js';
|
|
16
|
+
import type { CarrierStatusMap } from '../../core/status/statusMap.js';
|
|
16
17
|
/** The stage and status for a Correos `codEvento`, or undefined when unmapped. */
|
|
17
18
|
export declare function classifyCorreosSpainStatus(code: string): ClassifiedStatus | undefined;
|
|
19
|
+
/** What the map says about one scan, by its `codEvento`. */
|
|
20
|
+
export declare const statusMap: CarrierStatusMap;
|
|
@@ -63,3 +63,19 @@ const EVENT_STATUS = {
|
|
|
63
63
|
export function classifyCorreosSpainStatus(code) {
|
|
64
64
|
return EVENT_STATUS[code];
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The scans the review queue holds without their code, from before events
|
|
68
|
+
* kept it, by `normalizeStatusWording` wording. Each is the summary Correos
|
|
69
|
+
* gives that code.
|
|
70
|
+
*/
|
|
71
|
+
const UNCODED_SCANS = new Map([
|
|
72
|
+
['a disposición del destinatario', 'G01L020V'],
|
|
73
|
+
]);
|
|
74
|
+
/** What the map says about one scan, by its `codEvento`. */
|
|
75
|
+
export const statusMap = {
|
|
76
|
+
stage: (code, wording) => {
|
|
77
|
+
const key = code ? code.toLocaleUpperCase('en-US') : UNCODED_SCANS.get(wording);
|
|
78
|
+
return key ? classifyCorreosSpainStatus(key)?.stage : undefined;
|
|
79
|
+
},
|
|
80
|
+
gaps: [],
|
|
81
|
+
};
|
|
@@ -121,5 +121,6 @@ export const statusMap = {
|
|
|
121
121
|
{ code: 'SPE', note: 'A delivery estimate or a changed delivery day, not a scan.' },
|
|
122
122
|
{ code: 'MSDLO', note: 'The notice DPD emailed for the delivery round, not a scan.' },
|
|
123
123
|
{ code: 'MIDLI', note: "The recipient's delivery instruction, not a scan." },
|
|
124
|
+
{ code: 'ENA', note: "A data exchange inside DPD's systems, not a scan: it comes before the parcel reaches the origin depot." },
|
|
124
125
|
],
|
|
125
126
|
};
|
|
@@ -207,6 +207,14 @@
|
|
|
207
207
|
"confirmedBy": "live",
|
|
208
208
|
"firstSeen": "2026-10-05",
|
|
209
209
|
"note": "The recipient's delivery instruction, not a scan."
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"code": "ENA",
|
|
213
|
+
"wording": "We are exchanging data internally",
|
|
214
|
+
"language": "en",
|
|
215
|
+
"confirmedBy": "live",
|
|
216
|
+
"firstSeen": "2026-10-02",
|
|
217
|
+
"note": "A data exchange inside DPD's systems, not a scan; seen twice before the parcel reached the origin depot."
|
|
210
218
|
}
|
|
211
219
|
]
|
|
212
220
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* pages kept by the Internet Archive.
|
|
17
17
|
*/
|
|
18
18
|
import type { ClassifiedStatus } from '../../core/status/index.js';
|
|
19
|
+
import type { CarrierStatusMap } from '../../core/status/statusMap.js';
|
|
19
20
|
/**
|
|
20
21
|
* Lowercase, accent-free, punctuation-free form used for every wording
|
|
21
22
|
* comparison. A spacing accent typed for an apostrophe ("l´expéditeur")
|
|
@@ -24,3 +25,9 @@ import type { ClassifiedStatus } from '../../core/status/index.js';
|
|
|
24
25
|
export declare function comparableText(value: string): string;
|
|
25
26
|
export declare function includesAny(value: string, phrases: string[]): boolean;
|
|
26
27
|
export declare function classifyStatus(description: string): ClassifiedStatus;
|
|
28
|
+
/**
|
|
29
|
+
* What the map says about one scan. The trace page has no codes, so the
|
|
30
|
+
* wording is the whole key, and wording the map does not recognize gets no
|
|
31
|
+
* stage, as in the adapter.
|
|
32
|
+
*/
|
|
33
|
+
export declare const statusMap: CarrierStatusMap;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clean } from '../../core/transport/
|
|
1
|
+
import { clean } from '../../core/transport/text.js';
|
|
2
2
|
/**
|
|
3
3
|
* Lowercase, accent-free, punctuation-free form used for every wording
|
|
4
4
|
* comparison. A spacing accent typed for an apostrophe ("l´expéditeur")
|
|
@@ -106,3 +106,15 @@ export function classifyStatus(description) {
|
|
|
106
106
|
// package's "omit the stage when it is not mapped" rule; the adapter drops it.
|
|
107
107
|
return { status: 'unknown', stage: 'in_transit' };
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* What the map says about one scan. The trace page has no codes, so the
|
|
111
|
+
* wording is the whole key, and wording the map does not recognize gets no
|
|
112
|
+
* stage, as in the adapter.
|
|
113
|
+
*/
|
|
114
|
+
export const statusMap = {
|
|
115
|
+
stage: (code, wording) => {
|
|
116
|
+
const classified = code ? null : classifyStatus(wording);
|
|
117
|
+
return classified && classified.status !== 'unknown' ? classified.stage : undefined;
|
|
118
|
+
},
|
|
119
|
+
gaps: [],
|
|
120
|
+
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"carrier": "hermes",
|
|
3
3
|
"records": [
|
|
4
4
|
{
|
|
5
|
-
"number": "
|
|
5
|
+
"number": "62100000000000000",
|
|
6
6
|
"role": "shipment",
|
|
7
7
|
"evidence": "synthetic",
|
|
8
8
|
"derivedFrom": "official_shape",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"dhl-ecommerce"
|
|
15
15
|
]
|
|
16
16
|
},
|
|
17
|
-
"note": "Made-up number
|
|
17
|
+
"note": "Made-up number in the 17-digit parcel-number shape. Hermes rejects it, so it opens no order. Filed under Hermes Einrichtungs-Service, which no rule claims."
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|
|
@@ -9,7 +9,7 @@ import { NOOP_RECORDER } from '../../core/telemetry/index.js';
|
|
|
9
9
|
import { calendarDay } from '../../core/time/index.js';
|
|
10
10
|
import { clean, cleanScalar, decodeText, fetchBounded, parseJsonBytes, TRAWL_TRANSPORT_ALLOWANCE_MS, TrawlClient } from '../../core/transport/index.js';
|
|
11
11
|
import { isRecord } from '../../core/types.js';
|
|
12
|
-
import { UPS_PROGRESS_STATUS, upsActivityStage, upsStatus } from './status.js';
|
|
12
|
+
import { UPS_PROGRESS_STATUS, upsActivityStage, upsBannerStage, upsStatus } from './status.js';
|
|
13
13
|
const TRACKING_BASE = 'https://www.ups.com/track';
|
|
14
14
|
const STATUS_API = 'https://webapis.ups.com/track/api/Track/GetStatus?loc=en_US';
|
|
15
15
|
const TRAWL_PROVIDER = 'TRAWL while fetching UPS';
|
|
@@ -180,9 +180,10 @@ export function parseUPSTrackingHtml(page, trackingNumber) {
|
|
|
180
180
|
// town, not where the parcel is, so it never becomes the banner's place.
|
|
181
181
|
const location = clean($('#stApp_deliveredToAddress').last().text());
|
|
182
182
|
// The banner has no clock, so its event carries none.
|
|
183
|
+
const bannerStage = upsBannerStage(statusText);
|
|
183
184
|
const events = currentStatus === 'unknown'
|
|
184
185
|
? []
|
|
185
|
-
: [{ ...(location ? { location } : {}), description: statusText }];
|
|
186
|
+
: [{ ...(location ? { location } : {}), description: statusText, ...(bannerStage ? { stage: bannerStage } : {}) }];
|
|
186
187
|
return {
|
|
187
188
|
status: currentStatus,
|
|
188
189
|
last_status_text: statusText,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { CarrierStatus } from '../../core/result/index.js';
|
|
15
15
|
import type { Stage } from '../../core/status/index.js';
|
|
16
|
-
import type
|
|
16
|
+
import { type CarrierStatusMap } from '../../core/status/statusMap.js';
|
|
17
17
|
/**
|
|
18
18
|
* Classify UPS status prose. `hasEvents` reports that the shipment already has
|
|
19
19
|
* a scan history, which is what makes unrecognized wording "moving" rather
|
|
@@ -27,5 +27,7 @@ export declare function upsStatus(text: string, hasEvents?: boolean): CarrierSta
|
|
|
27
27
|
export declare const UPS_PROGRESS_STATUS: Readonly<Record<string, CarrierStatus>>;
|
|
28
28
|
/** The stage a scan's `actCode` stands for, or undefined when the code is not mapped. */
|
|
29
29
|
export declare function upsActivityStage(code: string): Stage | undefined;
|
|
30
|
-
/**
|
|
30
|
+
/** The stage of the rendered page's banner, or undefined for a milestone it does not name. */
|
|
31
|
+
export declare function upsBannerStage(text: string): Stage | undefined;
|
|
32
|
+
/** What the map says about one scan: by its activity code, or as the banner without one. */
|
|
31
33
|
export declare const statusMap: CarrierStatusMap;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeStatusWording } from '../../core/status/statusMap.js';
|
|
1
2
|
const EXCEPTION_TERMS = [
|
|
2
3
|
'return to sender', 'returned', 'delivery attempted', 'we missed you',
|
|
3
4
|
'not delivered', 'exception', 'action required',
|
|
@@ -76,8 +77,23 @@ const ACTIVITY_STAGES = new Map([
|
|
|
76
77
|
export function upsActivityStage(code) {
|
|
77
78
|
return ACTIVITY_STAGES.get(code.trim().toUpperCase());
|
|
78
79
|
}
|
|
79
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* The milestone the rendered page's banner names, the only wording the
|
|
82
|
+
* adapter reads without an activity code. Scan wording without its code is
|
|
83
|
+
* not read: "Arrived at Facility" is both the origin scan and an arrival.
|
|
84
|
+
*/
|
|
85
|
+
const BANNER_STAGES = new Map([
|
|
86
|
+
['label created', 'registered'],
|
|
87
|
+
['on the way', 'in_transit'],
|
|
88
|
+
['out for delivery', 'out_for_delivery'],
|
|
89
|
+
['delivered', 'delivered'],
|
|
90
|
+
]);
|
|
91
|
+
/** The stage of the rendered page's banner, or undefined for a milestone it does not name. */
|
|
92
|
+
export function upsBannerStage(text) {
|
|
93
|
+
return BANNER_STAGES.get(normalizeStatusWording(text));
|
|
94
|
+
}
|
|
95
|
+
/** What the map says about one scan: by its activity code, or as the banner without one. */
|
|
80
96
|
export const statusMap = {
|
|
81
|
-
stage: (code) => (code ? upsActivityStage(code) :
|
|
97
|
+
stage: (code, wording) => (code ? upsActivityStage(code) : upsBannerStage(wording)),
|
|
82
98
|
gaps: [],
|
|
83
99
|
};
|
|
@@ -49,17 +49,17 @@
|
|
|
49
49
|
{ "wording": "Not Delivered", "language": "en", "stage": "failed_attempt", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "Adapter result status: exception." },
|
|
50
50
|
{ "wording": "Exception", "language": "en", "stage": "failed_attempt", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "Adapter result status: exception." },
|
|
51
51
|
{ "wording": "Action Required", "language": "en", "stage": "failed_attempt", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "Adapter result status: exception." },
|
|
52
|
-
{ "wording": "Delivered", "language": "en", "stage": "delivered", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "" },
|
|
52
|
+
{ "wording": "Delivered", "language": "en", "stage": "delivered", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "As the rendered page's banner, which has no code, it gets this stage from the map." },
|
|
53
53
|
{ "wording": "Left at", "language": "en", "stage": "delivered", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "Prefix of UPS's \"Left at: Front Door\" wording; the location detail itself is never retained." },
|
|
54
|
-
{ "wording": "Out For Delivery", "language": "en", "stage": "out_for_delivery", "confirmedBy": "fixture", "firstSeen": "2026-09-12", "note": "Seen as \"Out For Delivery Today\" in fixtures/out-for-delivery.json." },
|
|
55
|
-
{ "wording": "On the Way", "language": "en", "stage": "in_transit", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "" },
|
|
54
|
+
{ "wording": "Out For Delivery", "language": "en", "stage": "out_for_delivery", "confirmedBy": "fixture", "firstSeen": "2026-09-12", "note": "Seen as \"Out For Delivery Today\" in fixtures/out-for-delivery.json. As the rendered page's banner, which has no code, it gets this stage from the map." },
|
|
55
|
+
{ "wording": "On the Way", "language": "en", "stage": "in_transit", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "As the rendered page's banner, which has no code, it gets this stage from the map." },
|
|
56
56
|
{ "wording": "In Transit", "language": "en", "stage": "in_transit", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "" },
|
|
57
57
|
{ "wording": "We Have Your Package", "language": "en", "stage": "in_transit", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "" },
|
|
58
58
|
{ "wording": "First UPS Possession", "language": "en", "stage": "accepted", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "Adapter result status: in_transit." },
|
|
59
59
|
{ "wording": "Departed", "language": "en", "stage": "in_transit", "confirmedBy": "fixture", "firstSeen": "2026-09-12", "note": "Seen as \"Departed from Facility\"." },
|
|
60
60
|
{ "wording": "Arrived", "language": "en", "stage": "in_transit", "confirmedBy": "fixture", "firstSeen": "2026-09-12", "note": "Seen as \"Arrived at Facility\"." },
|
|
61
61
|
{ "wording": "Processing at UPS Facility", "language": "en", "stage": "in_transit", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "" },
|
|
62
|
-
{ "wording": "Label Created", "language": "en", "stage": "
|
|
62
|
+
{ "wording": "Label Created", "language": "en", "stage": "registered", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "The prose reads it as status pending. As the rendered page's banner, which has no code, it gets this stage from the map." },
|
|
63
63
|
{ "wording": "Manifest Upload", "language": "en", "stage": "pending", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "" },
|
|
64
64
|
{ "wording": "Shipment Ready for UPS", "language": "en", "stage": "pending", "confirmedBy": "prior-art", "firstSeen": "2026-09-12", "note": "" },
|
|
65
65
|
{ "wording": "Delivered to UPS Access Point™ location and awaiting customer pickup.", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "prior-art", "firstSeen": "2026-09-20", "note": "Checked before the delivered wording, which the sentence also contains." }
|
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
* `statusMap` next to its map in `status.ts`; carriers without one answer
|
|
9
9
|
* `unknown`.
|
|
10
10
|
*/
|
|
11
|
+
import { statusMap as aliexpress } from '../../carriers/aliexpress/status.js';
|
|
11
12
|
import { statusMap as chronopost } from '../../carriers/chronopost/status.js';
|
|
13
|
+
import { statusMap as correosSpain } from '../../carriers/correos-spain/status.js';
|
|
12
14
|
import { statusMap as dhlExpress } from '../../carriers/dhl-express/status.js';
|
|
13
15
|
import { statusMap as dpd } from '../../carriers/dpd/status.js';
|
|
14
16
|
import { statusMap as dpdDe } from '../../carriers/dpd-de/status.js';
|
|
17
|
+
import { statusMap as dpdFr } from '../../carriers/dpd-fr/status.js';
|
|
15
18
|
import { statusMap as laPoste } from '../../carriers/la-poste/status.js';
|
|
16
19
|
import { statusMap as postlogistics } from '../../carriers/postlogistics/status.js';
|
|
17
20
|
import { statusMap as swissPost } from '../../carriers/swiss-post/status.js';
|
|
@@ -22,8 +25,8 @@ import { normalizeStatusWording } from '../status/statusMap.js';
|
|
|
22
25
|
import { CARRIER_DEFINITIONS } from './definitions.js';
|
|
23
26
|
/** By adapter: a carrier served by another's adapter, as Delivengo by La Poste's, reads that map. */
|
|
24
27
|
export const STATUS_MAPS = {
|
|
25
|
-
chronopost, 'dhl-express': dhlExpress, dpd, 'dpd-de': dpdDe, '
|
|
26
|
-
yunexpress,
|
|
28
|
+
aliexpress, chronopost, 'correos-spain': correosSpain, 'dhl-express': dhlExpress, dpd, 'dpd-de': dpdDe, 'dpd-fr': dpdFr,
|
|
29
|
+
'la-poste': laPoste, postlogistics, 'swiss-post': swissPost, tnt, ups, yunexpress,
|
|
27
30
|
};
|
|
28
31
|
const UNKNOWN = { kind: 'unknown' };
|
|
29
32
|
function carrierStatusMap(carrier) {
|
package/dist/places/hubs.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
[
|
|
2
2
|
{ "country": "FR", "names": ["Roissy"], "town": "Roissy-en-France", "airport": "CDG" },
|
|
3
3
|
{ "country": "GB", "names": ["Heathrow"], "town": "London", "airport": "LHR" },
|
|
4
|
-
{ "country": "GB", "names": ["East Midlands"], "town": "Castle Donington", "airport": "EMA" }
|
|
4
|
+
{ "country": "GB", "names": ["East Midlands"], "town": "Castle Donington", "airport": "EMA" },
|
|
5
|
+
{ "country": "PL", "names": ["Sękocin Stary"], "town": "Sękocin Stary", "latitude": 52.108, "longitude": 20.88, "source": "https://download.geonames.org/export/zip/PL.zip" }
|
|
5
6
|
]
|
package/dist/places/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import facilityList from './facilities.json' with { type: 'json' };
|
|
|
5
5
|
import hubList from './hubs.json' with { type: 'json' };
|
|
6
6
|
import { nameKey, nameKeys } from './names.mjs';
|
|
7
7
|
const DATA = new URL('./places.tsv.br', import.meta.url);
|
|
8
|
+
const PINCODES = new URL('./pincodes.json', import.meta.url);
|
|
8
9
|
// Words for facilities and services rather than places ("Agence DPD de La Crau", "PAKETZENTRUM", "ISC NEW YORK NY(USPS)").
|
|
9
10
|
const FACILITY_WORDS = new Set(`
|
|
10
11
|
agence agency air airport area branch bureau cedex cidex center centre centro centrum clasificacion colis
|
|
@@ -53,9 +54,11 @@ const HUB_THOUSANDS = 50;
|
|
|
53
54
|
const NAMESAKE_THOUSANDS = 100;
|
|
54
55
|
// Swiss Post scans end in the site's six-digit number: "Zürich Briefzentrum 801050".
|
|
55
56
|
const FACILITIES = new Map(facilityList.map((facility) => [`${facility.country}:${facility.code}`, facility]));
|
|
56
|
-
// Names carriers give a hub instead of its town
|
|
57
|
+
// Names carriers give a hub instead of its town ("ROISSY" is Paris-Charles de Gaulle, not
|
|
58
|
+
// Roissy-en-Brie), and hubs in villages the gazetteer is too coarse for ("SEKOCIN STARY").
|
|
57
59
|
const HUBS = new Map(hubList.flatMap((hub) => hub.names.map((name) => [nameKey(name), hub])));
|
|
58
60
|
let loaded = null;
|
|
61
|
+
let pincodes = null;
|
|
59
62
|
function gazetteer() {
|
|
60
63
|
if (loaded)
|
|
61
64
|
return loaded;
|
|
@@ -225,14 +228,16 @@ function cjkPhrases(field) {
|
|
|
225
228
|
}
|
|
226
229
|
return found;
|
|
227
230
|
}
|
|
228
|
-
/** The places a phrase can name. A hub's
|
|
231
|
+
/** The places a phrase can name. A hub's name replaces the towns of its country that share it. */
|
|
229
232
|
function candidates(data, key) {
|
|
230
233
|
const hub = HUBS.get(key);
|
|
231
|
-
|
|
234
|
+
// At its airport, or at its own point when its village is too small for the gazetteer.
|
|
235
|
+
const airport = hub?.airport ? data.airports.get(hub.airport) : undefined;
|
|
236
|
+
const point = airport ?? (hub?.latitude !== undefined && hub.longitude !== undefined ? { latitude: hub.latitude, longitude: hub.longitude } : undefined);
|
|
232
237
|
const towns = [data.keys.get(key) ?? []].flat().flatMap((entry) => {
|
|
233
238
|
const index = entry < 0 ? ~entry : entry;
|
|
234
239
|
const country = data.codes[data.country[index]];
|
|
235
|
-
if (
|
|
240
|
+
if (point && country === hub.country)
|
|
236
241
|
return [];
|
|
237
242
|
return [{
|
|
238
243
|
country, admin1: data.codes[data.admin1[index]] ?? '', admin2: data.codes[data.admin2[index]] ?? '',
|
|
@@ -240,11 +245,11 @@ function candidates(data, key) {
|
|
|
240
245
|
alternate: entry < 0, name: data.names[index],
|
|
241
246
|
}];
|
|
242
247
|
});
|
|
243
|
-
if (!
|
|
248
|
+
if (!hub || !point)
|
|
244
249
|
return towns;
|
|
245
250
|
return [...towns, {
|
|
246
|
-
country: hub.country, admin1: '', admin2: '', latitude:
|
|
247
|
-
thousands: HUB_THOUSANDS, alternate: false, name: hub.town, site: airport.name, hub: true,
|
|
251
|
+
country: hub.country, admin1: '', admin2: '', latitude: point.latitude, longitude: point.longitude,
|
|
252
|
+
thousands: HUB_THOUSANDS, alternate: false, name: hub.town, ...(airport ? { site: airport.name } : {}), hub: true,
|
|
248
253
|
}];
|
|
249
254
|
}
|
|
250
255
|
/**
|
|
@@ -316,6 +321,8 @@ export function locatePlace(location, hints = {}) {
|
|
|
316
321
|
const outer = regions.filter((region) => !regions.some((other) => other.field === region.field
|
|
317
322
|
&& other.start < region.end && other.end > region.start && other.end - other.start > region.end - region.start));
|
|
318
323
|
let best = null;
|
|
324
|
+
// Whether the text names a town at all, even one nothing confirms.
|
|
325
|
+
let namesTown = false;
|
|
319
326
|
for (const [fieldIndex, list] of fieldPhrases.entries()) {
|
|
320
327
|
for (const phrase of list) {
|
|
321
328
|
const own = data.regions.get(phrase.key) ?? [];
|
|
@@ -330,7 +337,9 @@ export function locatePlace(location, hints = {}) {
|
|
|
330
337
|
// ("Santa Catarina" on a Brazilian parcel).
|
|
331
338
|
const named = phrase.whole ? own.filter((region) => (!country || region.country === country)
|
|
332
339
|
&& (fieldIndex > 0 || region.country === country || hinted.includes(region.country))) : [];
|
|
333
|
-
|
|
340
|
+
const towns = candidates(data, phrase.key);
|
|
341
|
+
namesTown ||= towns.length > 0;
|
|
342
|
+
for (const place of towns) {
|
|
334
343
|
// A town may still be named like the region it lies in: Moscow in "…, Moscow",
|
|
335
344
|
// Sofia in its own "Sofia-Capital" rather than the province of Sofia around it.
|
|
336
345
|
const namesRegion = ` ${(data.regionNames.get(`${place.country}.${place.admin1}`) ?? []).join(' , ')} `.includes(` ${phrase.key} `);
|
|
@@ -380,6 +389,13 @@ export function locatePlace(location, hints = {}) {
|
|
|
380
389
|
...(place.site ? { site: place.site } : airport ? { site: airport.name } : {}),
|
|
381
390
|
};
|
|
382
391
|
}
|
|
392
|
+
// India Post ends an office's name with its PIN code, and abbreviates some names
|
|
393
|
+
// past reading ("KOL AP TMO 700052"): the PIN's town places a scan that names no
|
|
394
|
+
// town, when the scan or the parcel is in India. A town nothing confirmed is no
|
|
395
|
+
// abbreviation, and its six digits may be a Swiss Post site ("Daillens … 131070").
|
|
396
|
+
const pin = !namesTown && (country === 'IN' || (!country && hinted.includes('IN'))) ? pinPlace(data, rest) : null;
|
|
397
|
+
if (pin)
|
|
398
|
+
return pin;
|
|
383
399
|
// A region alone places its country: "Guangdong Province", or "Santa Catarina"
|
|
384
400
|
// on a Brazilian parcel, though Cape Verde has one too.
|
|
385
401
|
if (!country) {
|
|
@@ -423,6 +439,40 @@ function airportFor(data, text, field, town, written, country) {
|
|
|
423
439
|
return serving.length === 1 ? serving[0] : null;
|
|
424
440
|
}
|
|
425
441
|
const round = (value) => Math.round(value * 1000) / 1000;
|
|
442
|
+
/**
|
|
443
|
+
* The town of the Indian PIN code a scan ends with. A foreign airport's code, alone or
|
|
444
|
+
* after its country's, names an office of exchange abroad ("Office - FRA 400051",
|
|
445
|
+
* "Office - DEFRA …"): the PIN is then the Indian office's, not where the scan happened.
|
|
446
|
+
*/
|
|
447
|
+
function pinPlace(data, text) {
|
|
448
|
+
const pin = /(?:^|\s)([1-8]\d{5})$/.exec(text)?.[1];
|
|
449
|
+
// Russian, Chinese and other six-digit postcodes come with their own script.
|
|
450
|
+
if (!pin || /[^\x20-\x7e]/.test(text))
|
|
451
|
+
return null;
|
|
452
|
+
for (const [, prefix, code] of text.matchAll(/\b([A-Z]{2})?([A-Z]{3})\b/g)) {
|
|
453
|
+
const airport = FACILITY_WORDS.has(code.toLowerCase()) ? undefined : data.airports.get(code);
|
|
454
|
+
if (airport && airport.country !== 'IN' && (!prefix || prefix === airport.country))
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
pincodes ??= JSON.parse(readFileSync(PINCODES, 'utf8'));
|
|
458
|
+
const { ranges, towns } = pincodes;
|
|
459
|
+
const value = Number(pin);
|
|
460
|
+
let low = 0;
|
|
461
|
+
let high = ranges.length - 1;
|
|
462
|
+
while (low <= high) {
|
|
463
|
+
const middle = (low + high) >> 1;
|
|
464
|
+
const [first, last, town] = ranges[middle];
|
|
465
|
+
if (value < first)
|
|
466
|
+
high = middle - 1;
|
|
467
|
+
else if (value > last)
|
|
468
|
+
low = middle + 1;
|
|
469
|
+
else {
|
|
470
|
+
const [name, latitude, longitude] = towns[town];
|
|
471
|
+
return { latitude, longitude, precision: 'city', country: 'IN', name };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
426
476
|
/** A sorting centre the table knows, by its number and the town its name starts with. */
|
|
427
477
|
function facilityPlace(text) {
|
|
428
478
|
const match = /^(.*\S)\s+(\d{6})$/.exec(text);
|