hh-shipments 0.3.5 → 0.3.7
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/app.js +20 -7
- package/public/extract.js +82 -32
- package/public/index.html +4 -4
- package/src/extract.js +82 -32
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -169,7 +169,10 @@ async function ocrFile(file, statusEl) {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
async function handlePhotoFile(f) {
|
|
172
|
+
const target = $('photo-result');
|
|
172
173
|
try {
|
|
174
|
+
const url = URL.createObjectURL(f);
|
|
175
|
+
target.innerHTML = `<div class="review"><img src="${url}" style="max-width:100%;border-radius:10px"><p class="muted">📖 Reading screenshot…</p></div>`;
|
|
173
176
|
const text = await ocrFile(f, $('ocr-status'));
|
|
174
177
|
// Upload original for reference, then run the quick flow on OCR text
|
|
175
178
|
const fd = new FormData();
|
|
@@ -203,25 +206,31 @@ function pastedImageFiles(e) {
|
|
|
203
206
|
return files;
|
|
204
207
|
}
|
|
205
208
|
|
|
206
|
-
// Paste-text tab: pasting a screenshot runs OCR and extracts
|
|
207
|
-
//
|
|
209
|
+
// Paste-text tab: pasting a screenshot runs OCR and extracts automatically —
|
|
210
|
+
// no "Extract" tap needed. Plain text paste still lands in the textarea.
|
|
208
211
|
async function handlePastedImageFile(f) {
|
|
212
|
+
const target = $('quick-result');
|
|
209
213
|
try {
|
|
214
|
+
// Instant feedback: show a thumbnail + progress the moment the image lands
|
|
215
|
+
const url = URL.createObjectURL(f);
|
|
216
|
+
target.innerHTML = `<div class="review"><img src="${url}" style="max-width:100%;border-radius:10px"><p class="muted" id="ocr-live">📖 Reading screenshot…</p></div>`;
|
|
210
217
|
const text = await ocrFile(f, $('ocr-status-paste'));
|
|
218
|
+
const live = $('ocr-live');
|
|
219
|
+
if (live) live.textContent = '🔍 Finding tracking number…';
|
|
211
220
|
const fd = new FormData();
|
|
212
221
|
fd.append('image', f);
|
|
213
222
|
const up = await fetch('/api/intake/image', { method: 'POST', body: fd });
|
|
214
223
|
if (!up.ok) throw new Error('upload failed');
|
|
215
|
-
const r = await runQuick(text,
|
|
216
|
-
if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead(
|
|
217
|
-
} catch (err) { $('ocr-status-paste').textContent = '';
|
|
224
|
+
const r = await runQuick(text, target);
|
|
225
|
+
if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead(target, text);
|
|
226
|
+
} catch (err) { $('ocr-status-paste').textContent = ''; target.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
|
|
218
227
|
}
|
|
219
228
|
|
|
220
229
|
$('tab-paste').addEventListener('paste', async (e) => {
|
|
221
230
|
const files = pastedImageFiles(e);
|
|
222
231
|
if (!files.length) return; // text paste — let it land in the textarea
|
|
223
232
|
e.preventDefault();
|
|
224
|
-
$('paste-box').innerHTML = '📋 <span class="muted">or tap here
|
|
233
|
+
$('paste-box').innerHTML = '📋 <span class="muted">or tap here and paste a screenshot</span>';
|
|
225
234
|
for (const f of files) await handlePastedImageFile(f);
|
|
226
235
|
});
|
|
227
236
|
|
|
@@ -383,7 +392,11 @@ let searchT;
|
|
|
383
392
|
$('search').addEventListener('input', () => { clearTimeout(searchT); searchT = setTimeout(loadDash, 400); });
|
|
384
393
|
let searchAT;
|
|
385
394
|
$('search-arch').addEventListener('input', () => { clearTimeout(searchAT); searchAT = setTimeout(loadArchive, 400); });
|
|
386
|
-
$('btn-quick').onclick = () =>
|
|
395
|
+
$('btn-quick').onclick = () => {
|
|
396
|
+
const t = $('paste-text').value.trim();
|
|
397
|
+
if (!t) { $('quick-result').innerHTML = '<p class="muted">Paste the seller\'s text above first — or tap <b>📋 Paste screenshot</b>, screenshots extract on their own.</p>'; return; }
|
|
398
|
+
runQuick(t, $('quick-result'));
|
|
399
|
+
};
|
|
387
400
|
$('btn-manual').onclick = async () => {
|
|
388
401
|
$('manual-err').textContent = '';
|
|
389
402
|
try {
|
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) {
|
|
@@ -111,9 +183,6 @@
|
|
|
111
183
|
}
|
|
112
184
|
|
|
113
185
|
function extractOcrTolerant(text, found, seen) {
|
|
114
|
-
// Strict UPS structure: 1Z + 6-char shipper number + 2-digit service + 8-digit serial.
|
|
115
|
-
// Used to validate "1Z"-as-"12" OCR repairs so we don't invent false positives.
|
|
116
|
-
var UPS_STRICT = /^1Z[0-9A-Z]{6}[0-9]{2}[0-9]{8}$/;
|
|
117
186
|
function push(num, carrier, confidence, extra) {
|
|
118
187
|
num = normalize(num);
|
|
119
188
|
if (!num || seen[num]) return;
|
|
@@ -122,28 +191,9 @@
|
|
|
122
191
|
if (extra) for (var k in extra) c[k] = extra[k];
|
|
123
192
|
found.push(c);
|
|
124
193
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// contains a letter — real UPS shipper numbers do, random digit runs
|
|
129
|
-
// don't. Never invent numbers from thin air.
|
|
130
|
-
var cand = null;
|
|
131
|
-
if (/^12[0-9A-Z]{16}$/.test(ups)) cand = '1Z' + ups.slice(2);
|
|
132
|
-
else if (/^IZ[0-9A-Z]{16}$/i.test(ups)) cand = '1Z' + ups.slice(2);
|
|
133
|
-
if (!cand || !UPS_STRICT.test(cand)) return null;
|
|
134
|
-
if (!/[A-Z]/.test(cand.slice(2, 8))) return null;
|
|
135
|
-
return cand;
|
|
136
|
-
}
|
|
137
|
-
// a) UPS number split by spaces/dashes: "1Z 9999 99 ..." (or "12 ..."/"IZ ..." misreads)
|
|
138
|
-
var m, spacedUps = /\b(?:1Z|12|IZ)[\s-]?(?:[0-9A-Z][\s-]?){16}\b/gi;
|
|
139
|
-
while ((m = spacedUps.exec(text)) !== null) {
|
|
140
|
-
var ups = normalize(m[0]);
|
|
141
|
-
if (/^1Z[0-9A-Z]{16}$/.test(ups)) push(ups, 'ups', 'medium');
|
|
142
|
-
else {
|
|
143
|
-
var fixed = repairUpsZ(ups);
|
|
144
|
-
if (fixed) push(fixed, 'ups', 'medium');
|
|
145
|
-
}
|
|
146
|
-
}
|
|
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');
|
|
147
197
|
// b) long digit-like runs with OCR confusions -> fix -> re-match digit formats
|
|
148
198
|
var runRe = /[0-9OILSBoilszb]{10,24}/g, run;
|
|
149
199
|
while ((run = runRe.exec(text)) !== null) {
|
package/public/index.html
CHANGED
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
<button class="tab" data-tab="manual">Manual</button>
|
|
62
62
|
</div>
|
|
63
63
|
<div id="tab-paste" class="tabpanel">
|
|
64
|
-
<textarea id="paste-text" rows="
|
|
65
|
-
<div
|
|
66
|
-
<div class="
|
|
64
|
+
<textarea id="paste-text" rows="4" placeholder="Paste the seller's message here…"></textarea>
|
|
65
|
+
<div class="row" style="margin:8px 0"><button id="btn-clipboard" class="primary">📋 Paste screenshot — extracts automatically</button></div>
|
|
66
|
+
<div id="paste-box" class="pastebox" contenteditable="true" spellcheck="false">📋 <span class="muted">or tap here and paste a screenshot</span></div>
|
|
67
67
|
<div id="ocr-status-paste" class="muted"></div>
|
|
68
|
-
<button id="btn-quick" class="
|
|
68
|
+
<button id="btn-quick" class="secondary">Extract from text above</button>
|
|
69
69
|
<div id="quick-result"></div>
|
|
70
70
|
</div>
|
|
71
71
|
<div id="tab-photo" class="tabpanel" hidden>
|
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) {
|
|
@@ -111,9 +183,6 @@
|
|
|
111
183
|
}
|
|
112
184
|
|
|
113
185
|
function extractOcrTolerant(text, found, seen) {
|
|
114
|
-
// Strict UPS structure: 1Z + 6-char shipper number + 2-digit service + 8-digit serial.
|
|
115
|
-
// Used to validate "1Z"-as-"12" OCR repairs so we don't invent false positives.
|
|
116
|
-
var UPS_STRICT = /^1Z[0-9A-Z]{6}[0-9]{2}[0-9]{8}$/;
|
|
117
186
|
function push(num, carrier, confidence, extra) {
|
|
118
187
|
num = normalize(num);
|
|
119
188
|
if (!num || seen[num]) return;
|
|
@@ -122,28 +191,9 @@
|
|
|
122
191
|
if (extra) for (var k in extra) c[k] = extra[k];
|
|
123
192
|
found.push(c);
|
|
124
193
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// contains a letter — real UPS shipper numbers do, random digit runs
|
|
129
|
-
// don't. Never invent numbers from thin air.
|
|
130
|
-
var cand = null;
|
|
131
|
-
if (/^12[0-9A-Z]{16}$/.test(ups)) cand = '1Z' + ups.slice(2);
|
|
132
|
-
else if (/^IZ[0-9A-Z]{16}$/i.test(ups)) cand = '1Z' + ups.slice(2);
|
|
133
|
-
if (!cand || !UPS_STRICT.test(cand)) return null;
|
|
134
|
-
if (!/[A-Z]/.test(cand.slice(2, 8))) return null;
|
|
135
|
-
return cand;
|
|
136
|
-
}
|
|
137
|
-
// a) UPS number split by spaces/dashes: "1Z 9999 99 ..." (or "12 ..."/"IZ ..." misreads)
|
|
138
|
-
var m, spacedUps = /\b(?:1Z|12|IZ)[\s-]?(?:[0-9A-Z][\s-]?){16}\b/gi;
|
|
139
|
-
while ((m = spacedUps.exec(text)) !== null) {
|
|
140
|
-
var ups = normalize(m[0]);
|
|
141
|
-
if (/^1Z[0-9A-Z]{16}$/.test(ups)) push(ups, 'ups', 'medium');
|
|
142
|
-
else {
|
|
143
|
-
var fixed = repairUpsZ(ups);
|
|
144
|
-
if (fixed) push(fixed, 'ups', 'medium');
|
|
145
|
-
}
|
|
146
|
-
}
|
|
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');
|
|
147
197
|
// b) long digit-like runs with OCR confusions -> fix -> re-match digit formats
|
|
148
198
|
var runRe = /[0-9OILSBoilszb]{10,24}/g, run;
|
|
149
199
|
while ((run = runRe.exec(text)) !== null) {
|