universal-parcel-scraper 0.735.2-main.3769 → 0.736.0-main.3771
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.
|
@@ -72,27 +72,30 @@ const DEPOT_PLACES = [
|
|
|
72
72
|
];
|
|
73
73
|
const COUNTRY_PLACES = [
|
|
74
74
|
/\barrived in the destination country (.+?)(?: and is prepared\b|\.?$)/i,
|
|
75
|
-
/\
|
|
75
|
+
/\bfor further shipping \(([^()]+)\)/i,
|
|
76
76
|
];
|
|
77
77
|
// The town after the postcode closing a ParcelShop's address, before the
|
|
78
78
|
// collection deadline the ready-for-pickup wording appends.
|
|
79
79
|
const SHOP_TOWN = /,\s*\d{5}\s+([^\d,]+?)(?:\s+max\.\s.*|\s+abgegeben)?\.?\s*$/;
|
|
80
80
|
/**
|
|
81
81
|
* Where a row's wording puts the parcel. Hermes's own depots and ParcelShops
|
|
82
|
-
* are in Germany
|
|
82
|
+
* are in Germany. A partner's scan (`INTERNATIONAL_…`) names the country after
|
|
83
|
+
* its depot, "prepared in EXEMPLEVILLE for further shipping (France)",
|
|
84
|
+
* or the country alone.
|
|
83
85
|
*/
|
|
84
86
|
function wordedPlace(text, code) {
|
|
85
87
|
const place = (pattern) => clean(pattern.exec(text)?.[1], 120);
|
|
88
|
+
const country = COUNTRY_PLACES.map(place).find(Boolean) ?? '';
|
|
89
|
+
const partner = code.startsWith('INTERNATIONAL_');
|
|
86
90
|
for (const pattern of DEPOT_PLACES) {
|
|
87
91
|
const depot = place(pattern);
|
|
92
|
+
if (depot && partner)
|
|
93
|
+
return country ? `${depot}, ${country}` : depot;
|
|
88
94
|
if (depot)
|
|
89
95
|
return `${depot}, Germany`;
|
|
90
96
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (country)
|
|
94
|
-
return country;
|
|
95
|
-
}
|
|
97
|
+
if (country)
|
|
98
|
+
return country;
|
|
96
99
|
const town = code.startsWith('PARCELSHOP_') ? place(SHOP_TOWN) : '';
|
|
97
100
|
return town ? `${town}, Germany` : null;
|
|
98
101
|
}
|
|
@@ -4,6 +4,7 @@ import { normalizeCarrierResult } from '../result/index.js';
|
|
|
4
4
|
import { runSteps } from '../runner/index.js';
|
|
5
5
|
import { NOOP_RECORDER } from '../telemetry/index.js';
|
|
6
6
|
import { dateLocalClocks } from '../../places/clocks.js';
|
|
7
|
+
import { restoreLostLetters } from '../../places/index.js';
|
|
7
8
|
/**
|
|
8
9
|
* One carrier lookup, leaving provider selection and scheduling to the consumer.
|
|
9
10
|
* The adapter gets the normalized number, as from `createTracker()`: a number
|
|
@@ -25,7 +26,7 @@ export async function trackCarrier(carrier, typed, options) {
|
|
|
25
26
|
: await runSteps({ carrier, budgetMs: options.budgetMs ?? 120_000, signal: options.signal,
|
|
26
27
|
recorder: options.recorder ?? NOOP_RECORDER }, [{ id: registered.steps[0] ?? 'direct',
|
|
27
28
|
run: ({ signal, remainingMs }) => registered.track(input, ...(options.signal || options.budgetMs !== undefined ? [{ signal, budgetMs: context.budgetMs === undefined ? undefined : Math.min(context.budgetMs, remainingMs) }] : [])) }]);
|
|
28
|
-
return dateLocalClocks(carrier, normalizeCarrierResult(result));
|
|
29
|
+
return dateLocalClocks(carrier, restoreLostLetters(normalizeCarrierResult(result)));
|
|
29
30
|
}
|
|
30
31
|
catch (error) {
|
|
31
32
|
// A caller's cancellation is the caller's reason, not a carrier failure.
|
package/dist/places/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CarrierResult } from '../core/result/index.js';
|
|
1
2
|
export { carrierHomeZone, dateLocalClocks, type ZoneBasis } from './clocks.js';
|
|
2
3
|
/** Where a scan happened, as precisely as its free-text location allows. */
|
|
3
4
|
export interface EventPlace {
|
|
@@ -47,6 +48,11 @@ export declare function placeTimeZone(place: EventPlace): string | null;
|
|
|
47
48
|
* place we can be confident about. A wrong dot is worse than no dot.
|
|
48
49
|
*/
|
|
49
50
|
export declare function locatePlace(location: string | null | undefined, hints?: PlaceHints): EventPlace | null;
|
|
51
|
+
/**
|
|
52
|
+
* The result with the letters its scans' "Town, Country" places lost restored
|
|
53
|
+
* (`restoredTownName`), in the place and wherever the wording names it.
|
|
54
|
+
*/
|
|
55
|
+
export declare function restoreLostLetters(result: CarrierResult): CarrierResult;
|
|
50
56
|
/**
|
|
51
57
|
* Places for a parcel's scans, oldest first. A scan that names no country
|
|
52
58
|
* borrows one from the scans around it, then the destination and the
|
package/dist/places/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { brotliDecompressSync } from 'node:zlib';
|
|
3
3
|
import { CARRIER_DEFINITIONS } from '../core/catalog/definitions.js';
|
|
4
|
-
import { timeZoneCountry } from '../core/time/index.js';
|
|
4
|
+
import { countryCode, timeZoneCountry } from '../core/time/index.js';
|
|
5
5
|
import { ambiguousAddressCodes, militaryPostOffice, trackingPlace } from './trackingLocation.js';
|
|
6
6
|
import facilityList from './facilities.json' with { type: 'json' };
|
|
7
7
|
import hubList from './hubs.json' with { type: 'json' };
|
|
@@ -511,6 +511,70 @@ export function locatePlace(location, hints = {}) {
|
|
|
511
511
|
// Alone, a name two towns of a country share alike says neither.
|
|
512
512
|
return found && !found.twins ? found.place : null;
|
|
513
513
|
}
|
|
514
|
+
const LOST = '\uFFFD';
|
|
515
|
+
/**
|
|
516
|
+
* A town's name with the letters a carrier lost on the way, each a replacement
|
|
517
|
+
* character, filled from the one spelling of a town of `country` that fits:
|
|
518
|
+
* "SAINT-\uFFFDTIENNE" is Saint-Étienne's. A letter inside a word takes the
|
|
519
|
+
* case of the letters beside it, one starting a word the town's own. Null when
|
|
520
|
+
* no letter is lost or no single spelling fits.
|
|
521
|
+
*/
|
|
522
|
+
function restoredTownName(town, country) {
|
|
523
|
+
if (!town.includes(LOST))
|
|
524
|
+
return null;
|
|
525
|
+
const chars = [...town];
|
|
526
|
+
const source = chars.map((char) => char === LOST ? '(\\p{L})' : /[\s-]/u.test(char) ? '[\\s-]'
|
|
527
|
+
: char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('');
|
|
528
|
+
const pattern = new RegExp(`^${source}$`, 'iu');
|
|
529
|
+
const data = gazetteer();
|
|
530
|
+
const fills = new Set();
|
|
531
|
+
for (const index of countryPlaces(country)) {
|
|
532
|
+
const match = pattern.exec(data.names[index]);
|
|
533
|
+
if (match)
|
|
534
|
+
fills.add(match.slice(1).join(' '));
|
|
535
|
+
}
|
|
536
|
+
if (new Set([...fills].map((fill) => fill.toLowerCase())).size !== 1)
|
|
537
|
+
return null;
|
|
538
|
+
const letters = [...fills][0].split(' ');
|
|
539
|
+
const letter = (char) => char !== undefined && char !== LOST && /\p{L}/u.test(char);
|
|
540
|
+
return chars.map((char, at) => {
|
|
541
|
+
if (char !== LOST)
|
|
542
|
+
return char;
|
|
543
|
+
const filled = letters.shift();
|
|
544
|
+
if (!letter(chars[at - 1]))
|
|
545
|
+
return filled;
|
|
546
|
+
const beside = [chars[at - 1], chars[at + 1]].filter(letter);
|
|
547
|
+
return beside.every((other) => other === other.toUpperCase()) ? filled.toUpperCase() : filled.toLowerCase();
|
|
548
|
+
}).join('');
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* The result with the letters its scans' "Town, Country" places lost restored
|
|
552
|
+
* (`restoredTownName`), in the place and wherever the wording names it.
|
|
553
|
+
*/
|
|
554
|
+
export function restoreLostLetters(result) {
|
|
555
|
+
if (!result.events?.some((event) => event.location?.includes(LOST)))
|
|
556
|
+
return result;
|
|
557
|
+
const restored = new Map();
|
|
558
|
+
for (const event of result.events) {
|
|
559
|
+
const [, town, country] = /^(.+), ([^,]+)$/.exec(event.location ?? '') ?? [];
|
|
560
|
+
const code = country ? countryCode(country) : null;
|
|
561
|
+
const name = town && code && !restored.has(town) ? restoredTownName(town, code) : null;
|
|
562
|
+
if (name)
|
|
563
|
+
restored.set(town, name);
|
|
564
|
+
}
|
|
565
|
+
if (!restored.size)
|
|
566
|
+
return result;
|
|
567
|
+
const spell = (text) => [...restored].reduce((spelled, [lost, name]) => spelled.replaceAll(lost, name), text);
|
|
568
|
+
return {
|
|
569
|
+
...result,
|
|
570
|
+
...(result.last_status_text ? { last_status_text: spell(result.last_status_text) } : {}),
|
|
571
|
+
events: result.events.map((event) => ({
|
|
572
|
+
...event,
|
|
573
|
+
...(event.location ? { location: spell(event.location) } : {}),
|
|
574
|
+
...(event.description ? { description: spell(event.description) } : {}),
|
|
575
|
+
})),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
514
578
|
/** `split` reads a country the text names apart from its town; without it the whole text is the town's. */
|
|
515
579
|
function locate(location, hints, split = true) {
|
|
516
580
|
const text = location?.trim();
|
package/dist/server/openapi.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.736.0-main.3771",
|
|
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",
|