universal-parcel-scraper 0.3.0-main.306 → 0.3.0

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
@@ -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 1,671 such statuses, each filed under one stage.
152
+ The carrier folders record 1,672 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.
package/dist/app.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Helpers shaped for the parcel app this package was extracted from: its
3
3
  * parcel view, carrier-name hints for provider results, the clock helpers
4
- * its sync uses, and the country a scan's location names. They change with
5
- * that app and carry no stability promise; the tracking contract is the
6
- * other entry points.
4
+ * its sync uses, carrier scan-identity policies, and the country a scan's
5
+ * location names. They change with that app and carry no stability promise;
6
+ * the tracking contract is the other entry points.
7
7
  */
8
8
  export * from './core/catalog/parcel.js';
9
9
  export * from './core/catalog/hints.js';
10
10
  export * from './core/time/result.js';
11
+ export { sameInstantIdentityPolicy, type SameInstantIdentityPolicy } from './core/catalog/eventIdentity.js';
11
12
  export { universalCarrierHints } from './providers/shared/hints.js';
12
13
  export { recognitionAskedCarriers } from './core/catalog/recognition.js';
13
14
  export { countryFlag, countryName, trackingLocationCountry, trackingPlace, type TrackingPlace } from './places/trackingLocation.js';
package/dist/app.js CHANGED
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Helpers shaped for the parcel app this package was extracted from: its
3
3
  * parcel view, carrier-name hints for provider results, the clock helpers
4
- * its sync uses, and the country a scan's location names. They change with
5
- * that app and carry no stability promise; the tracking contract is the
6
- * other entry points.
4
+ * its sync uses, carrier scan-identity policies, and the country a scan's
5
+ * location names. They change with that app and carry no stability promise;
6
+ * the tracking contract is the other entry points.
7
7
  */
8
8
  export * from './core/catalog/parcel.js';
9
9
  export * from './core/catalog/hints.js';
10
10
  export * from './core/time/result.js';
11
+ export { sameInstantIdentityPolicy } from './core/catalog/eventIdentity.js';
11
12
  export { universalCarrierHints } from './providers/shared/hints.js';
12
13
  export { recognitionAskedCarriers } from './core/catalog/recognition.js';
13
14
  export { countryFlag, countryName, trackingLocationCountry, trackingPlace } from './places/trackingLocation.js';
@@ -0,0 +1,2 @@
1
+ import type { SameInstantIdentityPolicy } from '../../core/catalog/eventIdentity.js';
2
+ export declare const sameInstantIdentityPolicy: SameInstantIdentityPolicy;
@@ -0,0 +1,5 @@
1
+ // Verified and postcode-free replies reword the same scans at the same instants.
2
+ // A universal provider can also have stored those scans while DPD was unavailable.
3
+ export const sameInstantIdentityPolicy = {
4
+ sourceCarrierId: 'dpd', storedSources: ['dpd', 'unknown'], requireProviderCode: false,
5
+ };
@@ -109,6 +109,24 @@ const AIRPORT_ZONES = {
109
109
  LAX: 'America/Los_Angeles', SFO: 'America/Los_Angeles', YYZ: 'America/Toronto',
110
110
  SYD: 'Australia/Sydney', MEL: 'Australia/Melbourne',
111
111
  };
112
+ // Only verified names are expanded; other airport codes stay as the carrier wrote them.
113
+ const AIRPORT_PLACES = {
114
+ BOM: { name: 'Mumbai Airport', country: 'India' },
115
+ DEL: { name: 'Delhi Airport', country: 'India' },
116
+ FRA: { name: 'Frankfurt Airport', country: 'Germany' },
117
+ CDG: { name: 'Paris Charles de Gaulle Airport', country: 'France' },
118
+ };
119
+ function departureAirport(office) {
120
+ return /^Office - ([A-Z]{3})\b/.exec(office)?.[1];
121
+ }
122
+ /** Flight remarks have a narrow format; unrelated remarks can contain recipient details. */
123
+ function flightDescription(remarks, office) {
124
+ const match = /^Flight No:\s*([A-Z0-9]{2}\d{1,4}[A-Z]?)\s*\(From ([A-Z]{3}) To ([A-Z]{3})\)$/.exec(clean(remarks, 500));
125
+ if (!match || match[2] !== departureAirport(office))
126
+ return 'Aircraft Departure';
127
+ const airport = (code) => AIRPORT_PLACES[code] ? `${AIRPORT_PLACES[code].name} (${code})` : code;
128
+ return `Flight ${match[1]} departed: ${airport(match[2])} → ${airport(match[3])}`;
129
+ }
112
130
  /**
113
131
  * The zone a take-off row's clock is kept in. Its `tracked_at` is the
114
132
  * departure airport's wall clock under a UTC label: a take-off was seen
@@ -120,7 +138,7 @@ const AIRPORT_ZONES = {
120
138
  function takeOffZone(providerCode, office, trackedAt) {
121
139
  if (providerCode !== TAKE_OFF_CODE || !UTC_LABEL.test(trackedAt))
122
140
  return null;
123
- const airport = /^Office - ([A-Z]{3})\b/.exec(office)?.[1];
141
+ const airport = departureAirport(office);
124
142
  return airport ? AIRPORT_ZONES[airport] ?? null : null;
125
143
  }
126
144
  /**
@@ -172,13 +190,16 @@ export function parseIndiaPostTrackingHtml(html, trackingNumber) {
172
190
  const time = zone
173
191
  ? mislabeledLocalTime(rawEvent.tracked_at, zone, 100)
174
192
  : isoTime(rawEvent.tracked_at, 'Asia/Kolkata', 100);
175
- const description = eventText(clean(rawEvent.event));
193
+ const takeOff = providerCode === TAKE_OFF_CODE;
194
+ const description = takeOff ? flightDescription(rawEvent.remarks, office) : eventText(clean(rawEvent.event));
176
195
  if (!time || !description)
177
196
  return;
178
197
  const pincode = /^\d{6}$/.test(clean(rawEvent.pincode, 6))
179
198
  ? clean(rawEvent.pincode, 6)
180
199
  : '';
181
200
  const point = officePoint(rawEvent, office);
201
+ const airportCode = takeOff ? departureAirport(office) : undefined;
202
+ const airport = airportCode ? AIRPORT_PLACES[airportCode] : undefined;
182
203
  const identity = JSON.stringify([time.iso, description, office, pincode, providerCode]);
183
204
  if (seen.has(identity))
184
205
  return;
@@ -190,7 +211,7 @@ export function parseIndiaPostTrackingHtml(html, trackingNumber) {
190
211
  // deliberately never retained.
191
212
  event: {
192
213
  time: time.iso,
193
- location: [office, pincode].filter(Boolean).join(' '),
214
+ location: airport ? `${airport.name} (${airportCode}), ${airport.country}` : [office, pincode].filter(Boolean).join(' '),
194
215
  description,
195
216
  stage: classified.stage,
196
217
  ...(providerCode ? { provider_code: providerCode } : {}),
@@ -0,0 +1,2 @@
1
+ import type { SameInstantIdentityPolicy } from '../../core/catalog/eventIdentity.js';
2
+ export declare const sameInstantIdentityPolicy: SameInstantIdentityPolicy;
@@ -0,0 +1,4 @@
1
+ // MySpeedPost changes a scan's wording while its instant and event_type stay the same.
2
+ export const sameInstantIdentityPolicy = {
3
+ sourceCarrierId: 'india-post', storedSources: ['india-post'], requireProviderCode: true,
4
+ };
@@ -11,6 +11,8 @@ function includesAny(value, candidates) {
11
11
  * `in_transit` stage.
12
12
  */
13
13
  export function classifyIndiaPostEvent(...values) {
14
+ if (statusKey(values[0]) === 'aircrafttakeoff')
15
+ return { status: 'in_transit', stage: 'in_transit' };
14
16
  const key = values.map(statusKey).filter(Boolean).join(' ');
15
17
  if (includesAny(key, [
16
18
  'returntosender',
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "carrier": "india-post",
3
3
  "entries": [
4
+ { "code": "AircraftTakeOff", "wording": "Aircraft Departure / UPLIFT", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-04", "note": "Departure from the airport named by the office; flight remarks name the route, not a destination arrival." },
4
5
  { "code": "ItemBooked", "wording": "Item Booked", "language": "en", "stage": "accepted", "confirmedBy": "fixture", "firstSeen": "2026-09-01", "note": "Substring key `itembooked`; `articlebooked` and `bookingconfirmed` map the same way." },
5
6
  { "code": "ItemDispatched", "wording": "Item Dispatched", "language": "en", "stage": "in_transit", "confirmedBy": "fixture", "firstSeen": "2026-09-01", "note": "Substring key `itemdispatched`." },
6
7
  { "wording": "Item Bagged", "language": "en", "stage": "in_transit", "confirmedBy": "prior-art", "firstSeen": "2026-09-01", "note": "Substring key `itembagged`." },
@@ -0,0 +1,8 @@
1
+ /** Carrier evidence the parcel app uses when updating a stored scan in place. */
2
+ export interface SameInstantIdentityPolicy {
3
+ readonly sourceCarrierId: string;
4
+ readonly storedSources: readonly string[];
5
+ readonly requireProviderCode: boolean;
6
+ }
7
+ /** Unlisted sources cannot identify a reworded scan by its instant alone. */
8
+ export declare function sameInstantIdentityPolicy(sourceCarrierId: string): SameInstantIdentityPolicy | undefined;
@@ -0,0 +1,7 @@
1
+ import { sameInstantIdentityPolicy as dpd } from '../../carriers/dpd/app.js';
2
+ import { sameInstantIdentityPolicy as indiaPost } from '../../carriers/india-post/app.js';
3
+ const policies = new Map([dpd, indiaPost].map((policy) => [policy.sourceCarrierId, policy]));
4
+ /** Unlisted sources cannot identify a reworded scan by its instant alone. */
5
+ export function sameInstantIdentityPolicy(sourceCarrierId) {
6
+ return policies.get(sourceCarrierId);
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-parcel-scraper",
3
- "version": "0.3.0-main.306",
3
+ "version": "0.3.0",
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",