hh-shipments 0.3.4 → 0.3.6
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/package.json +1 -1
- package/public/extract.js +89 -13
- package/src/extract.js +89 -13
package/package.json
CHANGED
package/public/extract.js
CHANGED
|
@@ -56,10 +56,85 @@
|
|
|
56
56
|
return out;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Strict UPS structure: 1Z + 6-char shipper number + 2-digit service + 8-digit serial.
|
|
60
|
+
var UPS_STRICT = /^1Z[0-9A-Z]{6}[0-9]{2}[0-9]{8}$/;
|
|
61
|
+
function repairUpsZ(ups) {
|
|
62
|
+
// Tesseract often reads the "1Z" anchor as "12" (or "IZ"). Repair it
|
|
63
|
+
// only when the rest has strict UPS structure AND the shipper segment
|
|
64
|
+
// contains a letter — real UPS shipper numbers do, random digit runs
|
|
65
|
+
// don't. Never invent numbers from thin air.
|
|
66
|
+
var cand = null;
|
|
67
|
+
if (/^12[0-9A-Z]{16}$/.test(ups)) cand = '1Z' + ups.slice(2);
|
|
68
|
+
else if (/^IZ[0-9A-Z]{16}$/i.test(ups)) cand = '1Z' + ups.slice(2);
|
|
69
|
+
if (!cand || !UPS_STRICT.test(cand)) return null;
|
|
70
|
+
if (!/[A-Z]/.test(cand.slice(2, 8))) return null;
|
|
71
|
+
return cand;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// UPS number split by spaces/dashes: "1Z 9999 99 ..." (or "12 ..."/"IZ ..." misreads)
|
|
75
|
+
function spacedUpsIn(text, found, seen, conf) {
|
|
76
|
+
var m, re = /\b(?:1Z|12|IZ)[\s-]?(?:[0-9A-Z][\s-]?){16}\b/gi;
|
|
77
|
+
while ((m = re.exec(text)) !== null) {
|
|
78
|
+
var ups = normalize(m[0]);
|
|
79
|
+
var num = /^1Z[0-9A-Z]{16}$/.test(ups) ? ups : repairUpsZ(ups);
|
|
80
|
+
if (num && !seen[num]) {
|
|
81
|
+
seen[num] = true;
|
|
82
|
+
found.push({ number: num, carrier: 'ups', confidence: conf, ocr_fixed: num !== ups });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
59
87
|
function extractTrackingNumbers(text) {
|
|
60
88
|
var found = [];
|
|
61
89
|
var seen = {};
|
|
62
90
|
if (!text) return found;
|
|
91
|
+
// 0) ANCHOR PASS — on shipping labels the tracking number sits on or just
|
|
92
|
+
// under a line like "UPS GROUND" or "TRACKING #:". Scan those hot zones
|
|
93
|
+
// first so label structure decides, not blind whole-image regex. Anything
|
|
94
|
+
// found here outranks whole-image guesses (dedupe keeps the first hit).
|
|
95
|
+
anchorZones(text).forEach(function (z) {
|
|
96
|
+
runPatterns(z.text, found, seen, z.carrier);
|
|
97
|
+
spacedUpsIn(z.text, found, seen, 'high');
|
|
98
|
+
});
|
|
99
|
+
// 1) Whole-text strict patterns
|
|
100
|
+
runPatterns(text, found, seen, false);
|
|
101
|
+
// 2) OCR-tolerant recovery pass
|
|
102
|
+
extractOcrTolerant(text, found, seen);
|
|
103
|
+
var rank = { high: 0, medium: 1, low: 2 };
|
|
104
|
+
found.sort(function (a, b) { return rank[a.confidence] - rank[b.confidence]; });
|
|
105
|
+
return found;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Shipping-label structure: the tracking number lives on or just under
|
|
109
|
+
// lines like "UPS GROUND" / "TRACKING #:". Returns hot-zone chunks with the
|
|
110
|
+
// carrier named on/near the anchor (null when the anchor is generic).
|
|
111
|
+
function anchorCarrier(line) {
|
|
112
|
+
var l = String(line).toLowerCase();
|
|
113
|
+
if (/\bups\b/.test(l)) return 'ups';
|
|
114
|
+
if (/\bfed\s?ex\b/.test(l)) return 'fedex';
|
|
115
|
+
if (/\busps\b/.test(l) || /priority mail/.test(l)) return 'usps';
|
|
116
|
+
if (/\bdhl\b/.test(l)) return 'dhl';
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
function isAnchorLine(line) {
|
|
120
|
+
if (/track/.test(String(line).toLowerCase())) return true;
|
|
121
|
+
return anchorCarrier(line) !== null;
|
|
122
|
+
}
|
|
123
|
+
function anchorZones(text) {
|
|
124
|
+
var lines = String(text).split(/\r?\n/);
|
|
125
|
+
var zones = [];
|
|
126
|
+
for (var i = 0; i < lines.length; i++) {
|
|
127
|
+
if (!isAnchorLine(lines[i])) continue;
|
|
128
|
+
// anchor line + the two lines under it (the number is printed below)
|
|
129
|
+
var slice = lines.slice(i, Math.min(lines.length, i + 3));
|
|
130
|
+
var carrier = null;
|
|
131
|
+
slice.forEach(function (sl) { carrier = carrier || anchorCarrier(sl); });
|
|
132
|
+
zones.push({ text: slice.join('\n'), carrier: carrier });
|
|
133
|
+
}
|
|
134
|
+
return zones;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function runPatterns(text, found, seen, boostCarrier) {
|
|
63
138
|
PATTERNS.forEach(function (p) {
|
|
64
139
|
p.re.lastIndex = 0;
|
|
65
140
|
var m;
|
|
@@ -67,11 +142,13 @@
|
|
|
67
142
|
var num = normalize(m[1]);
|
|
68
143
|
if (seen[num]) continue;
|
|
69
144
|
seen[num] = true;
|
|
70
|
-
// Context boost: carrier word near the number
|
|
145
|
+
// Context boost: carrier word near the number, or a hot-zone anchor
|
|
146
|
+
// naming this carrier (a label's "UPS GROUND" line means the number
|
|
147
|
+
// under it really is UPS).
|
|
71
148
|
var idx = m.index;
|
|
72
149
|
var window = text.slice(Math.max(0, idx - 60), idx + 60 + m[1].length);
|
|
73
150
|
var confidence = p.confidence;
|
|
74
|
-
var boosted =
|
|
151
|
+
var boosted = !!(boostCarrier && p.carrier === boostCarrier);
|
|
75
152
|
CARRIER_WORDS.forEach(function (cw) {
|
|
76
153
|
if (cw.carrier === p.carrier && cw.re.test(window)) boosted = true;
|
|
77
154
|
});
|
|
@@ -80,11 +157,6 @@
|
|
|
80
157
|
found.push({ number: num, carrier: p.carrier, confidence: confidence });
|
|
81
158
|
}
|
|
82
159
|
});
|
|
83
|
-
// Sort: high confidence first, then by position
|
|
84
|
-
extractOcrTolerant(text, found, seen);
|
|
85
|
-
var rank = { high: 0, medium: 1, low: 2 };
|
|
86
|
-
found.sort(function (a, b) { return rank[a.confidence] - rank[b.confidence]; });
|
|
87
|
-
return found;
|
|
88
160
|
}
|
|
89
161
|
|
|
90
162
|
function isValidFormat(number, carrier) {
|
|
@@ -119,12 +191,9 @@
|
|
|
119
191
|
if (extra) for (var k in extra) c[k] = extra[k];
|
|
120
192
|
found.push(c);
|
|
121
193
|
}
|
|
122
|
-
// a) UPS number split by spaces/dashes
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
var ups = normalize(m[0]);
|
|
126
|
-
if (/^1Z[0-9A-Z]{16}$/.test(ups)) push(ups, 'ups', 'medium');
|
|
127
|
-
}
|
|
194
|
+
// a) UPS number split by spaces/dashes (whole-text fallback; the anchor
|
|
195
|
+
// pass already tried the hot zones at high confidence)
|
|
196
|
+
spacedUpsIn(text, found, seen, 'medium');
|
|
128
197
|
// b) long digit-like runs with OCR confusions -> fix -> re-match digit formats
|
|
129
198
|
var runRe = /[0-9OILSBoilszb]{10,24}/g, run;
|
|
130
199
|
while ((run = runRe.exec(text)) !== null) {
|
|
@@ -145,6 +214,13 @@
|
|
|
145
214
|
});
|
|
146
215
|
tokens.sort(function (a, b) { return b.length - a.length; });
|
|
147
216
|
tokens.slice(0, 3).forEach(function (t) {
|
|
217
|
+
// UPS "1Z" misread as "12"/"IZ" in a joined (space-free) token
|
|
218
|
+
var fixed = repairUpsZ(t);
|
|
219
|
+
if (fixed) { push(fixed, 'ups', 'medium'); return; }
|
|
220
|
+
// OCR-fixed variant may match a carrier format (e.g. I->1 at the start)
|
|
221
|
+
var digitFixed = ocrDigitFix(t);
|
|
222
|
+
var gc = guessCarrier(digitFixed);
|
|
223
|
+
if (gc !== 'unknown') { push(digitFixed, gc, 'low', { possible: true }); return; }
|
|
148
224
|
push(t, guessCarrier(t), 'low', { possible: true });
|
|
149
225
|
});
|
|
150
226
|
}
|
package/src/extract.js
CHANGED
|
@@ -56,10 +56,85 @@
|
|
|
56
56
|
return out;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Strict UPS structure: 1Z + 6-char shipper number + 2-digit service + 8-digit serial.
|
|
60
|
+
var UPS_STRICT = /^1Z[0-9A-Z]{6}[0-9]{2}[0-9]{8}$/;
|
|
61
|
+
function repairUpsZ(ups) {
|
|
62
|
+
// Tesseract often reads the "1Z" anchor as "12" (or "IZ"). Repair it
|
|
63
|
+
// only when the rest has strict UPS structure AND the shipper segment
|
|
64
|
+
// contains a letter — real UPS shipper numbers do, random digit runs
|
|
65
|
+
// don't. Never invent numbers from thin air.
|
|
66
|
+
var cand = null;
|
|
67
|
+
if (/^12[0-9A-Z]{16}$/.test(ups)) cand = '1Z' + ups.slice(2);
|
|
68
|
+
else if (/^IZ[0-9A-Z]{16}$/i.test(ups)) cand = '1Z' + ups.slice(2);
|
|
69
|
+
if (!cand || !UPS_STRICT.test(cand)) return null;
|
|
70
|
+
if (!/[A-Z]/.test(cand.slice(2, 8))) return null;
|
|
71
|
+
return cand;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// UPS number split by spaces/dashes: "1Z 9999 99 ..." (or "12 ..."/"IZ ..." misreads)
|
|
75
|
+
function spacedUpsIn(text, found, seen, conf) {
|
|
76
|
+
var m, re = /\b(?:1Z|12|IZ)[\s-]?(?:[0-9A-Z][\s-]?){16}\b/gi;
|
|
77
|
+
while ((m = re.exec(text)) !== null) {
|
|
78
|
+
var ups = normalize(m[0]);
|
|
79
|
+
var num = /^1Z[0-9A-Z]{16}$/.test(ups) ? ups : repairUpsZ(ups);
|
|
80
|
+
if (num && !seen[num]) {
|
|
81
|
+
seen[num] = true;
|
|
82
|
+
found.push({ number: num, carrier: 'ups', confidence: conf, ocr_fixed: num !== ups });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
59
87
|
function extractTrackingNumbers(text) {
|
|
60
88
|
var found = [];
|
|
61
89
|
var seen = {};
|
|
62
90
|
if (!text) return found;
|
|
91
|
+
// 0) ANCHOR PASS — on shipping labels the tracking number sits on or just
|
|
92
|
+
// under a line like "UPS GROUND" or "TRACKING #:". Scan those hot zones
|
|
93
|
+
// first so label structure decides, not blind whole-image regex. Anything
|
|
94
|
+
// found here outranks whole-image guesses (dedupe keeps the first hit).
|
|
95
|
+
anchorZones(text).forEach(function (z) {
|
|
96
|
+
runPatterns(z.text, found, seen, z.carrier);
|
|
97
|
+
spacedUpsIn(z.text, found, seen, 'high');
|
|
98
|
+
});
|
|
99
|
+
// 1) Whole-text strict patterns
|
|
100
|
+
runPatterns(text, found, seen, false);
|
|
101
|
+
// 2) OCR-tolerant recovery pass
|
|
102
|
+
extractOcrTolerant(text, found, seen);
|
|
103
|
+
var rank = { high: 0, medium: 1, low: 2 };
|
|
104
|
+
found.sort(function (a, b) { return rank[a.confidence] - rank[b.confidence]; });
|
|
105
|
+
return found;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Shipping-label structure: the tracking number lives on or just under
|
|
109
|
+
// lines like "UPS GROUND" / "TRACKING #:". Returns hot-zone chunks with the
|
|
110
|
+
// carrier named on/near the anchor (null when the anchor is generic).
|
|
111
|
+
function anchorCarrier(line) {
|
|
112
|
+
var l = String(line).toLowerCase();
|
|
113
|
+
if (/\bups\b/.test(l)) return 'ups';
|
|
114
|
+
if (/\bfed\s?ex\b/.test(l)) return 'fedex';
|
|
115
|
+
if (/\busps\b/.test(l) || /priority mail/.test(l)) return 'usps';
|
|
116
|
+
if (/\bdhl\b/.test(l)) return 'dhl';
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
function isAnchorLine(line) {
|
|
120
|
+
if (/track/.test(String(line).toLowerCase())) return true;
|
|
121
|
+
return anchorCarrier(line) !== null;
|
|
122
|
+
}
|
|
123
|
+
function anchorZones(text) {
|
|
124
|
+
var lines = String(text).split(/\r?\n/);
|
|
125
|
+
var zones = [];
|
|
126
|
+
for (var i = 0; i < lines.length; i++) {
|
|
127
|
+
if (!isAnchorLine(lines[i])) continue;
|
|
128
|
+
// anchor line + the two lines under it (the number is printed below)
|
|
129
|
+
var slice = lines.slice(i, Math.min(lines.length, i + 3));
|
|
130
|
+
var carrier = null;
|
|
131
|
+
slice.forEach(function (sl) { carrier = carrier || anchorCarrier(sl); });
|
|
132
|
+
zones.push({ text: slice.join('\n'), carrier: carrier });
|
|
133
|
+
}
|
|
134
|
+
return zones;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function runPatterns(text, found, seen, boostCarrier) {
|
|
63
138
|
PATTERNS.forEach(function (p) {
|
|
64
139
|
p.re.lastIndex = 0;
|
|
65
140
|
var m;
|
|
@@ -67,11 +142,13 @@
|
|
|
67
142
|
var num = normalize(m[1]);
|
|
68
143
|
if (seen[num]) continue;
|
|
69
144
|
seen[num] = true;
|
|
70
|
-
// Context boost: carrier word near the number
|
|
145
|
+
// Context boost: carrier word near the number, or a hot-zone anchor
|
|
146
|
+
// naming this carrier (a label's "UPS GROUND" line means the number
|
|
147
|
+
// under it really is UPS).
|
|
71
148
|
var idx = m.index;
|
|
72
149
|
var window = text.slice(Math.max(0, idx - 60), idx + 60 + m[1].length);
|
|
73
150
|
var confidence = p.confidence;
|
|
74
|
-
var boosted =
|
|
151
|
+
var boosted = !!(boostCarrier && p.carrier === boostCarrier);
|
|
75
152
|
CARRIER_WORDS.forEach(function (cw) {
|
|
76
153
|
if (cw.carrier === p.carrier && cw.re.test(window)) boosted = true;
|
|
77
154
|
});
|
|
@@ -80,11 +157,6 @@
|
|
|
80
157
|
found.push({ number: num, carrier: p.carrier, confidence: confidence });
|
|
81
158
|
}
|
|
82
159
|
});
|
|
83
|
-
// Sort: high confidence first, then by position
|
|
84
|
-
extractOcrTolerant(text, found, seen);
|
|
85
|
-
var rank = { high: 0, medium: 1, low: 2 };
|
|
86
|
-
found.sort(function (a, b) { return rank[a.confidence] - rank[b.confidence]; });
|
|
87
|
-
return found;
|
|
88
160
|
}
|
|
89
161
|
|
|
90
162
|
function isValidFormat(number, carrier) {
|
|
@@ -119,12 +191,9 @@
|
|
|
119
191
|
if (extra) for (var k in extra) c[k] = extra[k];
|
|
120
192
|
found.push(c);
|
|
121
193
|
}
|
|
122
|
-
// a) UPS number split by spaces/dashes
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
var ups = normalize(m[0]);
|
|
126
|
-
if (/^1Z[0-9A-Z]{16}$/.test(ups)) push(ups, 'ups', 'medium');
|
|
127
|
-
}
|
|
194
|
+
// a) UPS number split by spaces/dashes (whole-text fallback; the anchor
|
|
195
|
+
// pass already tried the hot zones at high confidence)
|
|
196
|
+
spacedUpsIn(text, found, seen, 'medium');
|
|
128
197
|
// b) long digit-like runs with OCR confusions -> fix -> re-match digit formats
|
|
129
198
|
var runRe = /[0-9OILSBoilszb]{10,24}/g, run;
|
|
130
199
|
while ((run = runRe.exec(text)) !== null) {
|
|
@@ -145,6 +214,13 @@
|
|
|
145
214
|
});
|
|
146
215
|
tokens.sort(function (a, b) { return b.length - a.length; });
|
|
147
216
|
tokens.slice(0, 3).forEach(function (t) {
|
|
217
|
+
// UPS "1Z" misread as "12"/"IZ" in a joined (space-free) token
|
|
218
|
+
var fixed = repairUpsZ(t);
|
|
219
|
+
if (fixed) { push(fixed, 'ups', 'medium'); return; }
|
|
220
|
+
// OCR-fixed variant may match a carrier format (e.g. I->1 at the start)
|
|
221
|
+
var digitFixed = ocrDigitFix(t);
|
|
222
|
+
var gc = guessCarrier(digitFixed);
|
|
223
|
+
if (gc !== 'unknown') { push(digitFixed, gc, 'low', { possible: true }); return; }
|
|
148
224
|
push(t, guessCarrier(t), 'low', { possible: true });
|
|
149
225
|
});
|
|
150
226
|
}
|