universal-parcel-scraper 0.120.0-main.1067 → 0.121.0-main.1069

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 **179 dedicated adapters** and **5 universal fallbacks**
16
+ **3,500+ carriers** through **180 dedicated adapters** and **5 universal fallbacks**
17
17
 
18
18
  <sub>208 catalog entries · 66 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 5,312 such statuses, each filed under one stage.
158
+ The carrier folders record 5,319 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
@@ -7649,7 +7649,9 @@
7649
7649
  "timezone": "America/Chicago",
7650
7650
  "tracking": {
7651
7651
  "mode": "automatic",
7652
- "adapter": "universal"
7652
+ "adapter": "osm-worldwide",
7653
+ "recognitionRank": 75,
7654
+ "localClocks": true
7653
7655
  },
7654
7656
  "canaryUrl": "https://www.osmworldwide.com/tracking/",
7655
7657
  "trackingUrlTemplate": "https://www.osmworldwide.com/tracking/?trackingIds={trackingNumber}",
@@ -7670,6 +7672,10 @@
7670
7672
  {
7671
7673
  "pattern": "^A\\d{10}[A-Z]{3}\\d{8}$",
7672
7674
  "confidence": "high"
7675
+ },
7676
+ {
7677
+ "pattern": "^OSM[0-9A-Z]{14}$",
7678
+ "confidence": "high"
7673
7679
  }
7674
7680
  ]
7675
7681
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "catalog": 208,
3
- "activeAdapters": 179,
3
+ "activeAdapters": 180,
4
4
  "countries": 66,
5
5
  "referenceCarriers": 100,
6
6
  "comparison": {
@@ -32841,6 +32841,22 @@
32841
32841
  "confidence": "none",
32842
32842
  "candidates": []
32843
32843
  },
32844
+ {
32845
+ "input": "OSM000000LM0060VO",
32846
+ "carrier": "osm-worldwide",
32847
+ "confidence": "high",
32848
+ "candidates": [
32849
+ "osm-worldwide"
32850
+ ]
32851
+ },
32852
+ {
32853
+ "input": "OSM000000LM00B0LO",
32854
+ "carrier": "osm-worldwide",
32855
+ "confidence": "high",
32856
+ "candidates": [
32857
+ "osm-worldwide"
32858
+ ]
32859
+ },
32844
32860
  {
32845
32861
  "input": "P2Q5J87J96",
32846
32862
  "carrier": "unknown",
@@ -6067,7 +6067,9 @@ var De = {
6067
6067
  timezone: "America/Chicago",
6068
6068
  tracking: {
6069
6069
  mode: "automatic",
6070
- adapter: "universal"
6070
+ adapter: "osm-worldwide",
6071
+ recognitionRank: 75,
6072
+ localClocks: !0
6071
6073
  },
6072
6074
  canaryUrl: "https://www.osmworldwide.com/tracking/",
6073
6075
  trackingUrlTemplate: "https://www.osmworldwide.com/tracking/?trackingIds={trackingNumber}",
@@ -6080,6 +6082,9 @@ var De = {
6080
6082
  detectionRules: [{
6081
6083
  pattern: "^A\\d{10}[A-Z]{3}\\d{8}$",
6082
6084
  confidence: "high"
6085
+ }, {
6086
+ pattern: "^OSM[0-9A-Z]{14}$",
6087
+ confidence: "high"
6083
6088
  }]
6084
6089
  },
6085
6090
  paquetexpress: {
@@ -7363,7 +7368,7 @@ var De = {
7363
7368
  "omniva-s10"
7364
7369
  ],
7365
7370
  "orlen-paczka": ["orlen-paczka-1"],
7366
- "osm-worldwide": ["osm-worldwide-1"],
7371
+ "osm-worldwide": ["osm-worldwide-1", "osm-worldwide-2"],
7367
7372
  paquetexpress: [],
7368
7373
  passport: ["passport-1"],
7369
7374
  "post-luxembourg": ["post-luxembourg-s10"],
@@ -12383,6 +12388,7 @@ var Qc = {
12383
12388
  omniva: 300,
12384
12389
  ontrac: 650,
12385
12390
  "orlen-paczka": 495,
12391
+ "osm-worldwide": 75,
12386
12392
  "poczta-polska": 750,
12387
12393
  "post-luxembourg": 80,
12388
12394
  "posta-slovenije": 130,
@@ -0,0 +1,16 @@
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
+ export declare const OSM_ENDPOINT = "https://prod.track-pub.osmworldwide.com/tracking/tracking-request";
5
+ /** The address the tracker asks for one number; without an account it takes one at a time. */
6
+ export declare function osmTrackingUrl(raw: string): string;
7
+ export declare class OsmWorldwideTracker {
8
+ private readonly options;
9
+ constructor(options?: {
10
+ fetcher?: typeof fetch;
11
+ recorder?: StepRecorder;
12
+ userAgent?: string;
13
+ });
14
+ fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
15
+ }
16
+ export declare const adapter: AdapterFactory;
@@ -0,0 +1,81 @@
1
+ import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
2
+ import { ChallengeError, IndeterminateError, InvalidInputError, RateLimitedError, 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 { isUspsNumber, normalizeOsmNumber, OSM_MAX_BYTES, parseOsmWorldwide, PROVIDER } from './parser.js';
8
+ // The service the tracking component on osmworldwide.com/tracking/ asks,
9
+ // without an account, key, cookie or CAPTCHA token.
10
+ export const OSM_ENDPOINT = 'https://prod.track-pub.osmworldwide.com/tracking/tracking-request';
11
+ // A missing route is not a missing parcel: an unknown number is the service's own `not-found`.
12
+ const GONE = [404, 410];
13
+ // CloudFront's or a firewall's block page instead of the service's JSON.
14
+ const CHALLENGE = /<title>\s*(?:Just a moment|Attention Required|Access denied|403 Forbidden|ERROR: The request could not be satisfied)|cf-turnstile|_cf_chl_opt|captcha/i;
15
+ function retryAfterMs(response) {
16
+ const header = response.headers.get('retry-after')?.trim();
17
+ return header && /^\d{1,6}$/.test(header) ? Number(header) * 1000 : undefined;
18
+ }
19
+ /** The address the tracker asks for one number; without an account it takes one at a time. */
20
+ export function osmTrackingUrl(raw) {
21
+ const url = new URL(OSM_ENDPOINT);
22
+ url.searchParams.set('ids', normalizeOsmNumber(raw));
23
+ return url.href;
24
+ }
25
+ export class OsmWorldwideTracker {
26
+ options;
27
+ constructor(options = {}) {
28
+ this.options = options;
29
+ }
30
+ async fetch(raw, context = {}) {
31
+ const url = osmTrackingUrl(raw);
32
+ const read = async (signal, timeoutMs) => {
33
+ // The service holds an anonymous reply for a few seconds before it answers.
34
+ const { response, bytes } = await fetchBounded(url, { method: 'GET', signal, headers: {
35
+ Accept: 'application/json', 'User-Agent': userAgentOf(this.options.userAgent),
36
+ } }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: OSM_MAX_BYTES,
37
+ fetcher: this.options.fetcher, allowHttpStatuses: [400, ...GONE, 429] });
38
+ const text = decodeText(bytes);
39
+ if (response.status === 429)
40
+ throw new RateLimitedError(PROVIDER, retryAfterMs(response));
41
+ if (GONE.includes(response.status))
42
+ throw new TransportError(PROVIDER, 'OSM Worldwide tracking service is unavailable');
43
+ let payload;
44
+ try {
45
+ payload = JSON.parse(text);
46
+ }
47
+ catch (cause) {
48
+ if (CHALLENGE.test(text.slice(0, 64_000)))
49
+ throw new ChallengeError(PROVIDER, 'OSM Worldwide answered with a block page', { cause });
50
+ throw new SchemaError(PROVIDER, 'OSM Worldwide returned a reply that is not JSON', { cause });
51
+ }
52
+ // The service refuses a request it cannot read, such as more numbers than
53
+ // an anonymous visitor may ask for, with HTTP 400; it says nothing about the number.
54
+ if (response.status === 400)
55
+ throw new IndeterminateError(PROVIDER, 'OSM Worldwide refused the tracking request');
56
+ return parseOsmWorldwide(payload, raw);
57
+ };
58
+ // A request that fails to reach OSM Worldwide, or hangs, gets one more try:
59
+ // the first gets half of the budget, so the retry fits in the rest.
60
+ return runSteps({ carrier: 'osm-worldwide', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
61
+ recorder: this.options.recorder ?? NOOP_RECORDER }, [
62
+ { id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
63
+ networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
64
+ ]);
65
+ }
66
+ }
67
+ /** Recognition never claims a USPS barcode: USPS issues it, whoever carries the parcel first. */
68
+ function recognizable(raw) {
69
+ const number = normalizeOsmNumber(raw);
70
+ if (isUspsNumber(number))
71
+ throw new InvalidInputError(PROVIDER, 'USPS barcodes belong to USPS');
72
+ return number;
73
+ }
74
+ export const adapter = (environment) => {
75
+ const tracker = new OsmWorldwideTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
76
+ return {
77
+ id: 'osm-worldwide', recordsSteps: true, steps: ['direct', 'retry'],
78
+ track: (input, context) => tracker.fetch(input.number, context),
79
+ recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => recognizable(number))),
80
+ };
81
+ };
@@ -17,18 +17,59 @@
17
17
  "timezone": "America/Chicago",
18
18
  "tracking": {
19
19
  "mode": "automatic",
20
- "adapter": "universal",
21
- "steps": []
20
+ "adapter": "osm-worldwide",
21
+ "steps": [
22
+ "direct",
23
+ "retry"
24
+ ],
25
+ "recognition": {
26
+ "rank": 75
27
+ },
28
+ "localClocks": true
22
29
  },
23
- "capabilities": [],
30
+ "capabilities": [
31
+ "history",
32
+ "location",
33
+ "eta",
34
+ "provider_code",
35
+ "delivery_partner"
36
+ ],
24
37
  "portal": {
25
38
  "url": "https://www.osmworldwide.com/tracking/?trackingIds={trackingNumber}",
26
39
  "searchUrl": "https://www.osmworldwide.com/tracking/",
27
40
  "canaryUrl": "https://www.osmworldwide.com/tracking/",
28
- "shows": [],
29
- "scraped": [],
30
- "discarded": [],
31
- "unavailable": []
41
+ "shows": [
42
+ "status",
43
+ "history",
44
+ "location",
45
+ "estimated_delivery",
46
+ "customer_reference",
47
+ "postcodes",
48
+ "delivery_instructions"
49
+ ],
50
+ "scraped": [
51
+ "status",
52
+ "history",
53
+ "location",
54
+ "estimated_delivery",
55
+ "provider_code"
56
+ ],
57
+ "discarded": [
58
+ "customer_reference",
59
+ "postcodes",
60
+ "delivery_instructions"
61
+ ],
62
+ "unavailable": [
63
+ "offset_timestamps",
64
+ "service_name",
65
+ "weight",
66
+ "dimensions",
67
+ "sender_name",
68
+ "recipient_name",
69
+ "recipient_address",
70
+ "proof_of_delivery",
71
+ "delivery_tracking_number"
72
+ ]
32
73
  },
33
74
  "links": [
34
75
  {
@@ -47,6 +88,11 @@
47
88
  "id": "osm-worldwide-1",
48
89
  "pattern": "^A\\d{10}[A-Z]{3}\\d{8}$",
49
90
  "confidence": "high"
91
+ },
92
+ {
93
+ "id": "osm-worldwide-2",
94
+ "pattern": "^OSM[0-9A-Z]{14}$",
95
+ "confidence": "high"
50
96
  }
51
97
  ]
52
98
  }
@@ -115,6 +115,44 @@
115
115
  },
116
116
  "note": "Looked up on OSM Worldwide's own tracker, as an archived address shows; the capture was not opened."
117
117
  },
118
+ {
119
+ "number": "OSM000000LM00B0LO",
120
+ "role": "shipment",
121
+ "evidence": "public_shipment_report",
122
+ "source": {
123
+ "url": "https://www.parcelpanel.com/couriers/osmworldwide/",
124
+ "date": "2026-10-12",
125
+ "dateKind": "capture",
126
+ "access": "full_page"
127
+ },
128
+ "context": {
129
+ "assessment": "confirmed"
130
+ },
131
+ "expect": {
132
+ "carrier": "osm-worldwide",
133
+ "confidence": "high"
134
+ },
135
+ "note": "ParcelPanel's OSM Worldwide page offered it as a sample to try on 12 Oct 2026. OSM Worldwide's tracking service answers it as its own package id, with scans from the label to the delivery."
136
+ },
137
+ {
138
+ "number": "OSM000000LM0060VO",
139
+ "role": "shipment",
140
+ "evidence": "public_shipment_report",
141
+ "source": {
142
+ "url": "https://www.parcelpanel.com/couriers/osmworldwide/",
143
+ "date": "2026-10-12",
144
+ "dateKind": "capture",
145
+ "access": "full_page"
146
+ },
147
+ "context": {
148
+ "assessment": "candidate"
149
+ },
150
+ "expect": {
151
+ "carrier": "osm-worldwide",
152
+ "confidence": "high"
153
+ },
154
+ "note": "ParcelPanel's OSM Worldwide page offered it as a sample to try on 12 Oct 2026; OSM Worldwide's tracking service no longer knows it."
155
+ },
118
156
  {
119
157
  "number": "BBSIPA00031503",
120
158
  "role": "shipment",
@@ -0,0 +1,20 @@
1
+ import type { CarrierResult } from '../../core/result/index.js';
2
+ export declare const PROVIDER = "OSM Worldwide";
3
+ export declare const OSM_MAX_BYTES: number;
4
+ /**
5
+ * A number as the tracker looks it up. A USPS routing barcode opens with the
6
+ * recipient's ZIP code, so only the package identifier after it is sent; one
7
+ * without a clean identifier is refused.
8
+ */
9
+ export declare function normalizeOsmNumber(raw: string): string;
10
+ /** Whether the number is a whole USPS package barcode, which USPS delivers. */
11
+ export declare function isUspsNumber(number: string): boolean;
12
+ /**
13
+ * The reply of `GET /tracking/tracking-request?ids={number}`: one entry per
14
+ * number asked for, here one. The entry names the number it answers, upper
15
+ * case, and is `found` with the parcel's scans newest first, or `not-found`.
16
+ * The customer package id beside it is the shipper's reference and is never
17
+ * read. A USPS package barcode is a parcel USPS delivers, so the result names
18
+ * USPS as the delivery partner, which tracks it under the same number.
19
+ */
20
+ export declare function parseOsmWorldwide(payload: unknown, raw: string): CarrierResult;
@@ -0,0 +1,156 @@
1
+ import { normalizeTrackingNumber, uspsPackageIdentifier } from '../../core/detection/index.js';
2
+ import { USPS_ROUTING_BARCODE } from '../../core/detection/usps.js';
3
+ import { IndeterminateError, InvalidInputError, NoHistoryError, NotFoundError, SchemaError } from '../../core/errors/index.js';
4
+ import { languageStageStatus } from '../../core/status/index.js';
5
+ import { calendarDay, explicitOffsetTime } from '../../core/time/index.js';
6
+ import { cleanScalar } from '../../core/transport/index.js';
7
+ import { isRecord } from '../../core/types.js';
8
+ import { osmStage } from './status.js';
9
+ export const PROVIDER = 'OSM Worldwide';
10
+ // A parcel's reply is a few kilobytes: a dozen short scans.
11
+ export const OSM_MAX_BYTES = 512 * 1024;
12
+ // The tracker takes any letters and digits: OSM's own package ids and the
13
+ // partner numbers it labels parcels with, most of them USPS barcodes.
14
+ const NUMBER = /^[A-Z0-9]{4,40}$/;
15
+ // A scan's clock as the service sends it, a wall time without an offset.
16
+ const WALL_CLOCK = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,7})?$/;
17
+ // An expected day, alone or at midnight.
18
+ const DAY = /^(\d{4})-(\d{2})-(\d{2})(?:T00:00:00(?:\.0{1,7})?)?$/;
19
+ const MAX_ROWS = 500;
20
+ const MAX_EVENTS = 100;
21
+ // The page shows no estimate once the parcel is delivered or after a failed attempt.
22
+ const NO_ESTIMATE = new Set(['delivered', 'returned', 'failed_attempt']);
23
+ /**
24
+ * A number as the tracker looks it up. A USPS routing barcode opens with the
25
+ * recipient's ZIP code, so only the package identifier after it is sent; one
26
+ * without a clean identifier is refused.
27
+ */
28
+ export function normalizeOsmNumber(raw) {
29
+ const compact = raw.length <= 64 ? normalizeTrackingNumber(raw) : '';
30
+ const number = USPS_ROUTING_BARCODE.test(compact) ? uspsPackageIdentifier(compact) ?? '' : compact;
31
+ if (!NUMBER.test(number))
32
+ throw new InvalidInputError(PROVIDER, 'OSM Worldwide tracks four to forty letters and digits');
33
+ return number;
34
+ }
35
+ /** Whether the number is a whole USPS package barcode, which USPS delivers. */
36
+ export function isUspsNumber(number) {
37
+ return uspsPackageIdentifier(number) === number;
38
+ }
39
+ /** An optional text field; an object, array or other value is a changed reply. */
40
+ function text(row, key, maxLength = 500) {
41
+ const value = row[key];
42
+ if (value === null || value === undefined)
43
+ return '';
44
+ if (typeof value !== 'string' && typeof value !== 'number')
45
+ throw new SchemaError(PROVIDER, `OSM Worldwide returned an invalid ${key}`);
46
+ return cleanScalar(value, maxLength);
47
+ }
48
+ /** An optional flag: absent is false, anything but a boolean is a changed reply. */
49
+ function flag(row, key) {
50
+ const value = row[key];
51
+ if (value === null || value === undefined)
52
+ return false;
53
+ if (typeof value !== 'boolean')
54
+ throw new SchemaError(PROVIDER, `OSM Worldwide returned an invalid ${key}`);
55
+ return value;
56
+ }
57
+ /** A scan's clock: an instant when it carries an offset, else its wall time. */
58
+ function clockOf(value) {
59
+ const instant = explicitOffsetTime(value);
60
+ if (instant)
61
+ return { time: instant.iso };
62
+ const match = WALL_CLOCK.exec(value);
63
+ if (!match)
64
+ return null;
65
+ const [, year, month, day, hour, minute, second] = match;
66
+ // Only the calendar fields are checked; no offset is assigned. Luxon accepts hour 24.
67
+ if (!calendarDay(Number(year), Number(month), Number(day)) || Number(hour) > 23 || Number(minute) > 59 || Number(second) > 59)
68
+ return null;
69
+ return { local_time: `${year}-${month}-${day}T${hour}:${minute}:${second}` };
70
+ }
71
+ /** The scan's town, state and country. A part holding a digit could be a street or a postcode, so it is left out. */
72
+ function placeOf(row) {
73
+ return ['city', 'state', 'country'].map((key) => text(row, key, 100))
74
+ .filter((part) => part && !/\d/.test(part)).join(', ');
75
+ }
76
+ /**
77
+ * One scan: its clock, wording, code, place and the tracker's flags. The
78
+ * postcode, the place line built with it and the scan's id, which the page
79
+ * sends back with delivery instructions, are never read.
80
+ */
81
+ function scanOf(row) {
82
+ if (!isRecord(row))
83
+ throw new SchemaError(PROVIDER, 'OSM Worldwide returned an invalid scan');
84
+ const description = text(row, 'description');
85
+ const clock = clockOf(text(row, 'datetime', 40));
86
+ if (!description || !clock)
87
+ throw new SchemaError(PROVIDER, 'OSM Worldwide returned a scan without its wording or clock');
88
+ const code = text(row, 'eventCode', 16);
89
+ const group = row.group === undefined || row.group === null ? 0 : row.group;
90
+ if (typeof group !== 'number' || !Number.isInteger(group))
91
+ throw new SchemaError(PROVIDER, 'OSM Worldwide returned an invalid group');
92
+ const location = placeOf(row);
93
+ const staged = osmStage(code, description, { group, instructionsNeeded: flag(row, 'instructionsNeeded'), isException: flag(row, 'isException') });
94
+ return { ...clock, description, ...(location ? { location } : {}), ...(code ? { provider_code: code } : {}),
95
+ ...(staged ? { stage: staged.stage, stage_source: staged.source } : {}) };
96
+ }
97
+ /** The expected day the page shows, while the parcel is still to be delivered. */
98
+ function expectedDay(value, stage) {
99
+ const match = DAY.exec(value);
100
+ if (!match || (stage && NO_ESTIMATE.has(stage)))
101
+ return null;
102
+ return calendarDay(Number(match[1]), Number(match[2]), Number(match[3]));
103
+ }
104
+ /**
105
+ * The reply of `GET /tracking/tracking-request?ids={number}`: one entry per
106
+ * number asked for, here one. The entry names the number it answers, upper
107
+ * case, and is `found` with the parcel's scans newest first, or `not-found`.
108
+ * The customer package id beside it is the shipper's reference and is never
109
+ * read. A USPS package barcode is a parcel USPS delivers, so the result names
110
+ * USPS as the delivery partner, which tracks it under the same number.
111
+ */
112
+ export function parseOsmWorldwide(payload, raw) {
113
+ const number = normalizeOsmNumber(raw);
114
+ if (!Array.isArray(payload) || payload.length !== 1 || !isRecord(payload[0])) {
115
+ throw new SchemaError(PROVIDER, 'OSM Worldwide returned an unexpected reply');
116
+ }
117
+ const entry = payload[0];
118
+ if (text(entry, 'id', 64).toUpperCase() !== number)
119
+ throw new SchemaError(PROVIDER, 'OSM Worldwide answered for another number');
120
+ const result = text(entry, 'result', 32);
121
+ if (result === 'not-found')
122
+ throw new NotFoundError(PROVIDER);
123
+ if (result !== 'found')
124
+ throw new IndeterminateError(PROVIDER, 'OSM Worldwide returned an unknown lookup result');
125
+ const rows = entry.history ?? [];
126
+ if (!Array.isArray(rows) || rows.length > MAX_ROWS)
127
+ throw new SchemaError(PROVIDER, 'OSM Worldwide returned an invalid parcel history');
128
+ // The reply lists scans newest first. Their clocks have no offset and come
129
+ // from several places, so its order stands; identical rows are kept once.
130
+ const seen = new Set();
131
+ const events = [];
132
+ for (const row of rows) {
133
+ const event = scanOf(row);
134
+ const key = JSON.stringify(event);
135
+ if (seen.has(key))
136
+ continue;
137
+ seen.add(key);
138
+ events.push(event);
139
+ }
140
+ if (!events.length)
141
+ throw new NoHistoryError(PROVIDER);
142
+ const newest = events[0];
143
+ const decider = events.find((event) => event.stage);
144
+ const stage = decider?.stage;
145
+ return {
146
+ status: stage ? languageStageStatus(stage) : 'unknown',
147
+ ...(stage ? { current_stage: stage, current_stage_source: decider.stage_source } : {}),
148
+ last_status_text: newest.description,
149
+ last_update: newest.time ?? null,
150
+ ...(newest.local_time ? { last_update_local: newest.local_time } : {}),
151
+ expected_delivery: expectedDay(text(entry, 'estDelivery', 40), stage),
152
+ ...(isUspsNumber(number) ? { delivery_carrier: 'usps' } : {}),
153
+ events: events.slice(0, MAX_EVENTS),
154
+ ...(events.length > MAX_EVENTS ? { history_truncated: true } : {}),
155
+ };
156
+ }
@@ -0,0 +1,23 @@
1
+ import { type ClassifiedWording, type Stage } from '../../core/status/index.js';
2
+ import { type CarrierStatusMap } from '../../core/status/statusMap.js';
3
+ /** The stage the map gives a scan's code, if it knows it. */
4
+ export declare function osmCodeStage(code: string): Stage | undefined;
5
+ /** The stage of a tracker milestone group, if the scan is in one. */
6
+ export declare function osmGroupStage(group: number): Stage | undefined;
7
+ /** The stage the map gives wording that came without a code it knows. */
8
+ export declare function osmWordingStage(wording: string): Stage | undefined;
9
+ /** What the tracker says about one scan besides its code and wording. */
10
+ export interface OsmScanFlags {
11
+ readonly group: number;
12
+ /** An attempted delivery the recipient can give instructions for. */
13
+ readonly instructionsNeeded: boolean;
14
+ readonly isException: boolean;
15
+ }
16
+ /**
17
+ * A scan's stage: its code, then the tracker's own flags and milestone group,
18
+ * then OSM's wording, then the shared wording rules. Wording none of them
19
+ * knows stays without a stage, so it remains visible for review.
20
+ */
21
+ export declare function osmStage(code: string, wording: string, flags: OsmScanFlags): ClassifiedWording | null;
22
+ /** What the map says about one scan, by its code, or by its wording when it has none. */
23
+ export declare const statusMap: CarrierStatusMap;
@@ -0,0 +1,66 @@
1
+ import { classifyWording } from '../../core/status/index.js';
2
+ import { normalizeStatusWording } from '../../core/status/statusMap.js';
3
+ /**
4
+ * The codes (`eventCode`) on OSM Worldwide's scans. The tracker gives the last
5
+ * mile partner's scans OSM codes of their own, "courier partner" in the
6
+ * wording. A partner that leaves with the parcel is on its way, not yet out
7
+ * for delivery: the same scan can come days before the delivery.
8
+ */
9
+ const CODES = {
10
+ OSM02: 'registered',
11
+ OSM04: 'in_transit',
12
+ OSM05: 'in_transit',
13
+ OSM06: 'in_transit',
14
+ OSM13: 'in_transit',
15
+ OSM01: 'delivered',
16
+ };
17
+ /**
18
+ * The tracker's milestone groups, as it names them: label created, in transit,
19
+ * out for delivery and delivered. Group 0 is a scan in none of them.
20
+ */
21
+ const GROUPS = {
22
+ 1: 'registered',
23
+ 2: 'in_transit',
24
+ 3: 'out_for_delivery',
25
+ 4: 'delivered',
26
+ };
27
+ /**
28
+ * OSM's FAQ says this scan is made when the shipper uploads the label, before
29
+ * OSM has the parcel; the shared rules would read it as a pickup.
30
+ */
31
+ const WORDINGS = [
32
+ [/^picked up by shipping partner$/, 'registered'],
33
+ ];
34
+ /** The stage the map gives a scan's code, if it knows it. */
35
+ export function osmCodeStage(code) {
36
+ return Object.hasOwn(CODES, code) ? CODES[code] : undefined;
37
+ }
38
+ /** The stage of a tracker milestone group, if the scan is in one. */
39
+ export function osmGroupStage(group) {
40
+ return Object.hasOwn(GROUPS, group) ? GROUPS[group] : undefined;
41
+ }
42
+ /** The stage the map gives wording that came without a code it knows. */
43
+ export function osmWordingStage(wording) {
44
+ const normalized = normalizeStatusWording(wording);
45
+ return WORDINGS.find(([pattern]) => pattern.test(normalized))?.[1];
46
+ }
47
+ /**
48
+ * A scan's stage: its code, then the tracker's own flags and milestone group,
49
+ * then OSM's wording, then the shared wording rules. Wording none of them
50
+ * knows stays without a stage, so it remains visible for review.
51
+ */
52
+ export function osmStage(code, wording, flags) {
53
+ const mapped = osmCodeStage(code)
54
+ ?? (flags.instructionsNeeded ? 'failed_attempt' : flags.isException ? 'exception' : undefined)
55
+ ?? osmGroupStage(flags.group)
56
+ ?? osmWordingStage(wording);
57
+ if (mapped)
58
+ return { stage: mapped, source: 'carrier_map' };
59
+ const worded = classifyWording(wording, 'in_transit');
60
+ return worded.source === 'none' ? null : worded;
61
+ }
62
+ /** What the map says about one scan, by its code, or by its wording when it has none. */
63
+ export const statusMap = {
64
+ stage: (code, wording) => (code ? osmCodeStage(code) : osmWordingStage(wording)),
65
+ gaps: [],
66
+ };
@@ -1,4 +1,12 @@
1
1
  {
2
2
  "carrier": "osm-worldwide",
3
- "entries": []
3
+ "entries": [
4
+ {"code": "OSM02", "wording": "Shipping label created - OSM awaiting item", "language": "en", "stage": "registered", "confirmedBy": "live", "firstSeen": "2026-10-12", "note": "The shipper's label, before OSM has the parcel."},
5
+ {"code": "OSM04", "wording": "Arrived at OSM facility", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-12", "note": "Each OSM facility the parcel reaches, the first as well."},
6
+ {"code": "OSM05", "wording": "Departed OSM facility", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-12"},
7
+ {"code": "OSM06", "wording": "Package arrived at courier partner", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-12", "note": "The hand-over to the last mile partner, which the tracker does not name."},
8
+ {"code": "OSM13", "wording": "Package left courier partner and is on its way", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-12", "note": "A last mile partner's scan. It can come days before the delivery, so it is not taken as out for delivery."},
9
+ {"code": "OSM01", "wording": "Package delivered", "language": "en", "stage": "delivered", "confirmedBy": "live", "firstSeen": "2026-10-12"},
10
+ {"wording": "Picked Up by Shipping Partner", "language": "en", "stage": "registered", "confirmedBy": "documentation", "note": "OSM's FAQ says this scan is made when the label is uploaded and does not mean OSM has the parcel."}
11
+ ]
4
12
  }
@@ -71,6 +71,7 @@ import { statusMap as oldDominion } from '../../carriers/old-dominion/status.js'
71
71
  import { statusMap as omgo } from '../../carriers/omgo/status.js';
72
72
  import { statusMap as omniva } from '../../carriers/omniva/status.js';
73
73
  import { statusMap as orlenPaczka } from '../../carriers/orlen-paczka/status.js';
74
+ import { statusMap as osmWorldwide } from '../../carriers/osm-worldwide/status.js';
74
75
  import { statusMap as postLuxembourg } from '../../carriers/post-luxembourg/status.js';
75
76
  import { statusMap as postaSlovenije } from '../../carriers/posta-slovenije/status.js';
76
77
  import { statusMap as postaSrbije } from '../../carriers/posta-srbije/status.js';
@@ -109,7 +110,8 @@ export const STATUS_MAPS = {
109
110
  'gofo-fr': gofoFr, 'gofo-it': gofoIt, helthjem, 'hrvatska-posta': hrvatskaPosta, 'j-and-t': jAndT, 'j-and-t-cargo': jntCargo, jitsu,
110
111
  'kuehne-nagel': kuehneNagel, 'la-poste': laPoste, 'latvijas-pasts': latvijasPasts, 'lietuvos-pastas': lietuvosPastas,
111
112
  'loomis-express': loomisExpress, maltapost, matkahuolto, meest, 'mng-kargo': mngKargo, nationex, 'old-dominion': oldDominion, omgo, omniva,
112
- 'orlen-paczka': orlenPaczka, 'post-luxembourg': postLuxembourg, 'posta-slovenije': postaSlovenije, 'posta-srbije': postaSrbije, postlogistics, ppl,
113
+ 'orlen-paczka': orlenPaczka, 'osm-worldwide': osmWorldwide,
114
+ 'post-luxembourg': postLuxembourg, 'posta-slovenije': postaSlovenije, 'posta-srbije': postaSrbije, postlogistics, ppl,
113
115
  ptt, sagawa, sameday, 'slovak-parcel-service': slovakParcelService, 'slovenska-posta': slovenskaPosta, speedx, speedy,
114
116
  'stallion-express': stallionExpress, sto, 'swiss-post': swissPost, 'thailand-post': thailandPost, tnt, ups, venipak, 'vinted-go': vintedGo,
115
117
  whistl, xdp, yunexpress, 'yurtici-kargo': yurticiKargo, zeleris,
@@ -7649,7 +7649,9 @@
7649
7649
  "timezone": "America/Chicago",
7650
7650
  "tracking": {
7651
7651
  "mode": "automatic",
7652
- "adapter": "universal"
7652
+ "adapter": "osm-worldwide",
7653
+ "recognitionRank": 75,
7654
+ "localClocks": true
7653
7655
  },
7654
7656
  "canaryUrl": "https://www.osmworldwide.com/tracking/",
7655
7657
  "trackingUrlTemplate": "https://www.osmworldwide.com/tracking/?trackingIds={trackingNumber}",
@@ -7670,6 +7672,10 @@
7670
7672
  {
7671
7673
  "pattern": "^A\\d{10}[A-Z]{3}\\d{8}$",
7672
7674
  "confidence": "high"
7675
+ },
7676
+ {
7677
+ "pattern": "^OSM[0-9A-Z]{14}$",
7678
+ "confidence": "high"
7673
7679
  }
7674
7680
  ]
7675
7681
  },
@@ -5148,7 +5148,9 @@ export declare const CARRIER_CATALOG: {
5148
5148
  readonly timezone: "America/Chicago";
5149
5149
  readonly tracking: {
5150
5150
  readonly mode: "automatic";
5151
- readonly adapter: "universal";
5151
+ readonly adapter: "osm-worldwide";
5152
+ readonly recognitionRank: 75;
5153
+ readonly localClocks: true;
5152
5154
  };
5153
5155
  readonly canaryUrl: "https://www.osmworldwide.com/tracking/";
5154
5156
  readonly trackingUrlTemplate: "https://www.osmworldwide.com/tracking/?trackingIds={trackingNumber}";
@@ -5161,6 +5163,9 @@ export declare const CARRIER_CATALOG: {
5161
5163
  readonly detectionRules: readonly [{
5162
5164
  readonly pattern: "^A\\d{10}[A-Z]{3}\\d{8}$";
5163
5165
  readonly confidence: "high";
5166
+ }, {
5167
+ readonly pattern: "^OSM[0-9A-Z]{14}$";
5168
+ readonly confidence: "high";
5164
5169
  }];
5165
5170
  };
5166
5171
  readonly paquetexpress: {
@@ -7650,7 +7650,9 @@ export const CARRIER_CATALOG = {
7650
7650
  "timezone": "America/Chicago",
7651
7651
  "tracking": {
7652
7652
  "mode": "automatic",
7653
- "adapter": "universal"
7653
+ "adapter": "osm-worldwide",
7654
+ "recognitionRank": 75,
7655
+ "localClocks": true
7654
7656
  },
7655
7657
  "canaryUrl": "https://www.osmworldwide.com/tracking/",
7656
7658
  "trackingUrlTemplate": "https://www.osmworldwide.com/tracking/?trackingIds={trackingNumber}",
@@ -7671,6 +7673,10 @@ export const CARRIER_CATALOG = {
7671
7673
  {
7672
7674
  "pattern": "^A\\d{10}[A-Z]{3}\\d{8}$",
7673
7675
  "confidence": "high"
7676
+ },
7677
+ {
7678
+ "pattern": "^OSM[0-9A-Z]{14}$",
7679
+ "confidence": "high"
7674
7680
  }
7675
7681
  ]
7676
7682
  },
@@ -9195,7 +9201,7 @@ export const DETECTION_RULE_IDS = {
9195
9201
  "omgo": ["omgo-1"],
9196
9202
  "omniva": ["omniva-1", "omniva-2", "omniva-s10"],
9197
9203
  "orlen-paczka": ["orlen-paczka-1"],
9198
- "osm-worldwide": ["osm-worldwide-1"],
9204
+ "osm-worldwide": ["osm-worldwide-1", "osm-worldwide-2"],
9199
9205
  "paquetexpress": [],
9200
9206
  "passport": ["passport-1"],
9201
9207
  "post-luxembourg": ["post-luxembourg-s10"],
@@ -79,6 +79,7 @@ export const CARRIER_RECOGNITION_RANKS = {
79
79
  "omniva": 300,
80
80
  "ontrac": 650,
81
81
  "orlen-paczka": 495,
82
+ "osm-worldwide": 75,
82
83
  "poczta-polska": 750,
83
84
  "post-luxembourg": 80,
84
85
  "posta-slovenije": 130,
@@ -123,6 +123,7 @@ import { adapter as omgo } from '../carriers/omgo/adapter.js';
123
123
  import { adapter as omniva } from '../carriers/omniva/adapter.js';
124
124
  import { adapter as ontrac } from '../carriers/ontrac/adapter.js';
125
125
  import { adapter as orlenPaczka } from '../carriers/orlen-paczka/adapter.js';
126
+ import { adapter as osmWorldwide } from '../carriers/osm-worldwide/adapter.js';
126
127
  import { adapter as paack } from '../carriers/paack/adapter.js';
127
128
  import { adapter as packeta } from '../carriers/packeta/adapter.js';
128
129
  import { adapter as planzer } from '../carriers/planzer/adapter.js';
@@ -304,6 +305,7 @@ export const REGISTRY = {
304
305
  "omniva": omniva,
305
306
  "ontrac": ontrac,
306
307
  "orlen-paczka": orlenPaczka,
308
+ "osm-worldwide": osmWorldwide,
307
309
  "paack": paack,
308
310
  "packeta": packeta,
309
311
  "planzer": planzer,
@@ -498,7 +500,7 @@ export const REGISTRY = {
498
500
  "omniva": "omniva",
499
501
  "ontrac": "ontrac",
500
502
  "orlen-paczka": "orlen-paczka",
501
- "osm-worldwide": "universal",
503
+ "osm-worldwide": "osm-worldwide",
502
504
  "paack": "paack",
503
505
  "packeta": "packeta",
504
506
  "paquetexpress": "universal",
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.120.0"
5
+ "version": "0.121.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.120.0-main.1067",
3
+ "version": "0.121.0-main.1069",
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",