PageSourceSearch

https://modafinil.pl/wp-content/plugins/parcel-selector/map/kasamuka/js/kasamuka-search-engine.js?ver=3.14.3

js modafinil.pl collected 2026-10-09 22:26:50 UTC 32,485 bytes, 693 lines download raw bytes

1/**
2 * KASAMUKA Search Engine v1.1.0
3 * Semantic search for KASAMUKA PRO map (p.n / p.a / p.l point format)
4 *
5 * Supports 23 European countries:
6 *   PL DE FR IT ES PT NL BE AT HU LU GB CZ SK SE RO HR CH SI LT LV EE BG
7 *
8 * Features:
9 *   - Intent detection: LOCKER_CODE_EXACT / CITY_POSTAL / POSTAL_ONLY /
10 *                       CITY_STREET / CITY_ONLY / STREET_ONLY / FALLBACK
11 *   - Per-country postal code formats (PL 12-345, DE 12345, NL 1234AB,
12 *     GB SW1A2AA, SE 123 45, CH 1234, PT 1234-567, BE 1000, HU 1234 ...)
13 *   - Per-country street prefix noise words (ul./rue/str./via/calle ...)
14 *   - Full EU diacritics normalization (Latin only — no Cyrillic)
15 *   - Scoring: postal+400, city+300, street+200, lockerId+500
16 *
17 * API:
18 *   window.KasamukaSearchEngine.search(points, rawQuery, {countryCode, limit})
19 *   → { results: [...], meta: { type, intent, conflict } }
20 */
21;(function (window) {
22    'use strict';
23
24    var SearchEngine = {};
25
26    // ── EU diacritics map (Latin only — 23 countries) ─────────────────────────
27    var DIACRITICS_MAP = {
28        // Polish
29        ą:'a', ć:'c', ę:'e', ł:'l', ń:'n', ó:'o', ś:'s', ź:'z', ż:'z',
30        // German / Austrian / Swiss
31        ä:'a', ö:'o', ü:'u', ß:'ss',
32        // French / Belgian / Luxembourg
33        à:'a', â:'a', é:'e', è:'e', ê:'e', ë:'e', ï:'i', î:'i',
34        ô:'o', ù:'u', û:'u', ü:'u', ÿ:'y', ç:'c', œ:'oe', æ:'ae',
35        // Spanish / Portuguese
36        á:'a', í:'i', ú:'u', ñ:'n', ã:'a', õ:'o',
37        // Czech / Slovak
38        ř:'r', š:'s', č:'c', ž:'z', ý:'y', ď:'d', ť:'t', ň:'n', ě:'e', ů:'u',
39        // Hungarian
40        ő:'o', ű:'u',
41        // Romanian
42        ș:'s', ț:'t', ş:'s', ţ:'t',
43        // Croatian / Slovenian
44        // (č, š, ž already covered above)
45        // Swedish / Finnish
46        å:'a',
47        // Lithuanian
48        ū:'u', ė:'e', į:'i',
49        // Latvian
50        ā:'a', ē:'e', ī:'i', ģ:'g', ķ:'k', ļ:'l', ņ:'n', ŗ:'r',
51        // Estonian
52        // (ä, ö, ü already covered above)
53        // GB / IE — no diacritics needed
54    };
55
56    // ── Per-country street-prefix noise words ─────────────────────────────────
57    // These are stripped from queries/point data before token matching.
58    var NOISE_WORDS_COMMON = {
59        // PL
60        ul:1, 'ul.':1, ulica:1, al:1, 'al.':1, aleja:1, pl:1, plac:1, os:1, 'os.':1,
61        // DE / AT / CH
62        str:1, 'str.':1, strasse:1, straße:1, gasse:1, weg:1, platz:1, ring:1,
63        // FR / BE / LU
64        rue:1, avenue:1, 'av.':1, boulevard:1, 'bd.':1, impasse:1, place:1,
65        // IT
66        via:1, viale:1, corso:1, piazza:1, vicolo:1,
67        // ES / PT
68        calle:1, 'c/':1, avenida:1, paseo:1, rua:1, largo:1, travessa:1,
69        // NL / BE (nl)
70        straat:1, laan:1, weg:1, plein:1, kade:1, gracht:1,
71        // SE
72        gatan:1, vagen:1, vägen:1, torget:1,
73        // CZ / SK
74        namesti:1, náměstí:1, trida:1, třída:1,
75        // HU
76        utca:1, ut:1, út:1, ter:1, tér:1,
77        // GB
78        street:1, road:1, avenue:1, lane:1, drive:1, close:1, court:1, way:1,
79        // RO / HR / SI
80        strada:1, bulevardul:1, calea:1, ulica:1,
81        // Generic
82        nr:1, 'nr.':1, no:1, 'no.':1,
83    };
84
85    // ── Per-country postal code patterns ──────────────────────────────────────
86    // Ordered by specificity (most specific first).
87    // Each entry: { cc: ['XX',...], pattern: RegExp, normalize: fn }
88    var POSTAL_PATTERNS = [
89        // PL: 12-345 or 12 345 or 12345 (with or without separator)
90        { cc: ['PL'], re: /\b(\d{2})[-\s]?(\d{3})\b(?!\d)/, norm: function(m) { return m[1]+m[2]; } },
91        // GB: SW1A 2AA, EC1A 1BB, W1A 0AX etc.
92        { cc: ['GB'], re: /\b([A-Z]{1,2}\d{1,2}[A-Z]?)\s?(\d[A-Z]{2})\b/i, norm: function(m) { return (m[1]+m[2]).toUpperCase(); } },
93        // NL: 1234 AB (Dutch format — letters suffix; BE does NOT use this format)
94        { cc: ['NL'], re: /\b(\d{4})\s?([A-Z]{2})\b/i, norm: function(m) { return (m[1]+m[2]).toUpperCase(); } },
95        // PT: 1234-567
96        { cc: ['PT'], re: /\b(\d{4})[-\s](\d{3})\b/, norm: function(m) { return m[1]+m[2]; } },
97        // SE: 123 45
98        { cc: ['SE'], re: /\b(\d{3})\s(\d{2})\b/, norm: function(m) { return m[1]+m[2]; } },
99        // CH: 4 digits (1000-9999)
100        { cc: ['CH'], re: /\b([1-9]\d{3})\b/, norm: function(m) { return m[1]; } },
101        // HU/AT/LU/BE: 4 digits (1000-9999)
102        // AT: 1010-9992, LU: 1000-9999, BE: 1000-9999 — all 4-digit, no letter suffix
103        { cc: ['HU','AT','LU','BE'], re: /\b([1-9]\d{3})\b/, norm: function(m) { return m[1]; } },
104        // LT/LV/EE: LT-12345 or 5 digits
105        { cc: ['LT','LV','EE'], re: /\b(?:[A-Z]{2}-)?(\d{5})\b/i, norm: function(m) { return m[1]; } },
106        // SI/HR: 4 digits (1000-9999)
107        { cc: ['SI','HR'], re: /\b([1-9]\d{3})\b/, norm: function(m) { return m[1]; } },
108        // DE/FR/IT/ES/CZ/SK/RO/BG: 5 digits
109        { cc: ['DE','FR','IT','ES','CZ','SK','RO','BG'], re: /\b(\d{5})\b/, norm: function(m) { return m[1]; } },
110        // Fallback: 5 digits
111        { cc: null, re: /\b(\d{5})\b/, norm: function(m) { return m[1]; } },
112        // Fallback: 4 digits
113        { cc: null, re: /\b(\d{4})\b/, norm: function(m) { return m[1]; } },
114    ];
115
116    // ── Normalization ─────────────────────────────────────────────────────────
117
118    function normalizeText(text) {
119        if (!text) return '';
120        return String(text).toLowerCase().replace(/\s+/g, ' ').trim();
121    }
122
123    function normalizeDiacritics(text) {
124        if (!text) return '';
125        return String(text).replace(/[^\u0000-\u007E]/g, function (c) {
126            var lc = c.toLowerCase();
127            return DIACRITICS_MAP[lc] || DIACRITICS_MAP[c] || c;
128        });
129    }
130
131    function sanitizeQuery(raw) {
132        if (!raw) return '';
133        var q = String(raw)
134            .replace(/[\r\n\t]+/g, ' ')
135            .replace(/['"\\<>]+/g, '')
136            .replace(/[,;|\s]+$/g, '')
137            .replace(/\s+/g, ' ').trim();
138        return q;
139    }
140
141    // ── Postal code detection — country-aware ─────────────────────────────────
142
143    function detectPostal(raw, countryCode) {
144        var cc = countryCode ? String(countryCode).toUpperCase() : null;
145        // 'UK' is the country key used in anunaki-countries.js; postal patterns use 'GB'.
146        if (cc === 'UK') cc = 'GB';
147        for (var i = 0; i < POSTAL_PATTERNS.length; i++) {
148            var pp = POSTAL_PATTERNS[i];
149            if (pp.cc && cc && pp.cc.indexOf(cc) === -1) continue;
150            var m = raw.match(pp.re);
151            if (m) return { original: m[0], normalized: pp.norm(m) };
152        }
153        return null;
154    }
155
156    function stripPostal(raw) {
157        // Strip any known postal pattern
158        return raw
159            .replace(/\b[A-Z]{1,2}\d{1,2}[A-Z]?\s?\d[A-Z]{2}\b/gi, ' ')  // GB
160            .replace(/\b\d{4}\s?[A-Z]{2}\b/gi, ' ')                         // NL
161            .replace(/\b[A-Z]{2}-\d{5}\b/gi, ' ')                           // LT/LV/EE prefix
162            .replace(/\b\d{2}[-\s]\d{3}\b/, ' ')                             // PL
163            .replace(/\b\d{4}[-\s]\d{3}\b/, ' ')                             // PT
164            .replace(/\b\d{3}\s\d{2}\b/, ' ')                                // SE
165            .replace(/\b\d{5}\b/, ' ')                                        // 5-digit
166            .replace(/\b\d{4}\b/, ' ')                                        // 4-digit
167            .replace(/\s+/g, ' ').trim();
168    }
169
170    function detectLockerId(raw) {
171        if (!raw) return null;
172        // Require at least one digit — prevents city names like "WARS" matching as locker IDs.
173        // Real locker IDs are always alphanumeric with digits: KRA01A, WAW04B, ZOL01, etc.
174        var re = /^(?=.*\d)[A-Z0-9]{4,10}$/;
175        var compact = raw.replace(/\s+/g, '').toUpperCase();
176        if (re.test(compact)) return compact;
177        var parts = raw.split(/\s+/);
178        for (var i = 0; i < parts.length; i++) {
179            var t = parts[i].toUpperCase();
180            if (re.test(t)) return t;
181        }
182        return null;
183    }
184
185    // ── Tokenization ─────────────────────────────────────────────────────────
186
187    function splitByCityAndStreet(raw) {
188        // Explicit separator: comma, pipe, semicolon
189        var sepMatch = raw.match(/(.*?)(,|\||;)(.*)/);
190        if (sepMatch) return { cityPart: sepMatch[1], streetPart: sepMatch[3] };
191        // Street prefix keywords (multi-language).
192        // Use (?=\W|$) lookahead instead of \b — dotted abbreviations like "ul." end with
193        // a non-word char, so \b never fires after them (space after dot = \W+\W, no boundary).
194        // Alternatives ordered longest→shortest to prevent "ul" swallowing "ulica".
195        var ulMatch = raw.match(
196            /(.*?)\b(ulica|ul\.|ul|aleja|al\.|al|plac|pl\.|os\.|rue|via|calle|strasse|stra\u00dfe|str\.|str|straat|gatan|utca|strada|rua)(?=\W|$)(.*)/i
197        );
198        if (ulMatch) return { cityPart: ulMatch[1], streetPart: ulMatch[3] };
199        return { cityPart: raw, streetPart: '' };
200    }
201
202    function tokensFrom(text) {
203        if (!text) return [];
204        return normalizeDiacritics(normalizeText(text))
205            .split(' ')
206            .filter(function (t) { return t.length > 0; });
207    }
208
209    function filterNoiseTokens(tokens) {
210        if (!tokens || !tokens.length) return [];
211        return tokens.filter(function (t) { return !NOISE_WORDS_COMMON[t]; });
212    }
213
214    function parseQuery(raw, countryCode) {
215        var sanitized   = sanitizeQuery(raw);
216        var normalized  = normalizeText(sanitized);
217        var ascii       = normalizeDiacritics(normalized);
218        var postal      = detectPostal(normalized, countryCode) || detectPostal(ascii, countryCode);
219        var withoutPostal = postal ? stripPostal(normalized) : normalized;
220        var lockerId    = detectLockerId(normalized);
221        var parts       = splitByCityAndStreet(withoutPostal);
222        var cityTokens  = filterNoiseTokens(tokensFrom(parts.cityPart).slice(0, 3));
223        var streetTokens = filterNoiseTokens(tokensFrom(parts.streetPart));
224        var buildingNumber = null;
225        if (streetTokens.length > 0) {
226            var last = streetTokens[streetTokens.length - 1];
227            if (/^\d+[a-zA-Z]?$/.test(last)) {
228                buildingNumber = last;
229                streetTokens.pop();
230            }
231        }
232        return {
233            raw: raw, sanitized: sanitized, normalized: normalized, ascii: ascii,
234            lockerId: lockerId, postal: postal,
235            cityTokens: cityTokens, streetTokens: streetTokens, buildingNumber: buildingNumber
236        };
237    }
238
239    // ── KASAMUKA point field accessors (p.n / p.a / p.l format) ──────────────
240
241    function pointCountry(point) {
242        if (!point) return null;
243        var c = point.country ? String(point.country).toUpperCase() : null;
244        // Normalize 'UK' alias → 'GB' so filterByCountry and detectPostal are consistent
245        return c === 'UK' ? 'GB' : c;
246    }
247
248    function pointPostal(point, countryCode) {
249        if (point._postal !== undefined) return point._postal;
250        var addr = point.a || '';
251        var r = detectPostal(addr, countryCode || pointCountry(point));
252        return r ? r.normalized : '';
253    }
254
255    /**
256     * Extract CITY tokens from point address.
257     * p.l is always a description (or null) — city is in the LAST comma-segment
258     * of p.a after stripping the postal code.
259     *
260     * Examples across all countries:
261     *   PL "Kościuszki 27, 21-412 Adamów"           → last seg "21-412 Adamów" → "Adamów"
262     *   DE "Baustr. 21a, 17438 Wolgast"              → last seg "17438 Wolgast" → "Wolgast"
263     *   FR "RUE DU GENERAL DE GAULLE, 76240 BELBEUF" → last seg "76240 BELBEUF" → "BELBEUF"
264     *   UK "Oxford St, SW1A 2AA London"              → last seg "SW1A 2AA London" → "London"
265     *   IT "Via Montello 5, 67051 Avezzano"          → last seg "67051 Avezzano" → "Avezzano"
266     *   ES "CARRER DE BINEFAR 3, 08020 BARCELONA"    → last seg "08020 BARCELONA" → "BARCELONA"
267     *   NL "51 Raadstede, 3431 HA Nieuwegein"        → last seg "3431 HA Nieuwegein" → "Nieuwegein"
268     */
269    function pointCityTokens(point) {
270        if (point._cityTokens) return point._cityTokens;
271        var addr = point.a || '';
272        var segs = addr.split(',');
273        var last = (segs[segs.length - 1] || '').trim();
274        var city = stripPostal(last).trim();
275        // If stripping ate the entire segment, try second-to-last
276        if (!city && segs.length > 1) {
277            city = stripPostal((segs[segs.length - 2] || '').trim()).trim();
278        }
279        return tokensFrom(city);
280    }
281
282    /**
283     * Full address tokens — ALL segments of p.a after stripping postal code.
284     * Broader than pointCityTokens — includes street words too.
285     * Used as fallback when city extraction from last segment fails
286     * (unusual address formats, missing commas, etc.).
287     */
288    function pointAddrTokens(point) {
289        if (point._addrTokens) return point._addrTokens;
290        var raw = stripPostal(normalizeText(point.a || ''));
291        return filterNoiseTokens(tokensFrom(raw));
292    }
293
294    function pointStreetTokens(point) {
295        if (point._streetTokens) return point._streetTokens;
296        // p.a = full address; street is the first comma-segment (before postal+city)
297        var raw = point.a || '';
298        raw = stripPostal(raw).split(',')[0].trim();
299        return filterNoiseTokens(tokensFrom(raw));
300    }
301
302    // ── Matching helpers ──────────────────────────────────────────────────────
303
304    function tokensMatchAll(queryTokens, targetTokens) {
305        if (!queryTokens.length || !targetTokens.length) return false;
306        return queryTokens.every(function (qt) {
307            return targetTokens.some(function (tt) { return tt.indexOf(qt) !== -1; });
308        });
309    }
310
311    function tokensMatchAny(queryTokens, targetTokens) {
312        if (!queryTokens.length || !targetTokens.length) return false;
313        return queryTokens.some(function (qt) {
314            return targetTokens.some(function (tt) { return tt.indexOf(qt) !== -1; });
315        });
316    }
317
318    /**
319     * Exact whole-word city match — each query token must equal a target token exactly.
320     * Used for city filtering so "Wrocław" doesn't match "Inowrocław" or "Wrocławskie".
321     * Substring matching is kept as fallback for partial/incomplete queries.
322     */
323    function tokensMatchAllExact(queryTokens, targetTokens) {
324        if (!queryTokens.length || !targetTokens.length) return false;
325        return queryTokens.every(function (qt) {
326            return targetTokens.some(function (tt) { return tt === qt; });
327        });
328    }
329
330    function filterByCountry(points, countryCode) {
331        if (!countryCode) return points.slice();
332        var cc = String(countryCode).toUpperCase();
333        if (cc === 'UK') cc = 'GB'; // normalize alias — pointCountry() also normalizes UK→GB
334        return points.filter(function (p) {
335            var pc = pointCountry(p);
336            return !pc || pc === cc;
337        });
338    }
339
340    // ── Intent evaluation ─────────────────────────────────────────────────────
341
342    function evaluateIntent(parsed, points, countryCode) {
343        var inCountry = filterByCountry(points, countryCode);
344
345        var lockerPoint = null;
346        if (parsed.lockerId) {
347            var lid = parsed.lockerId;
348            lockerPoint = inCountry.filter(function (p) {
349                return p.n && String(p.n).toUpperCase().replace(/\s+/g, '') === lid;
350            })[0] || null;
351        }
352
353        var postalPoints = [];
354        if (parsed.postal) {
355            var pnorm = parsed.postal.normalized;
356            postalPoints = inCountry.filter(function (p) {
357                return pointPostal(p, countryCode) === pnorm;
358            });
359        }
360
361        var cityPoints = [];
362        if (parsed.cityTokens.length > 0) {
363            // Exact whole-word match first: "Wrocław" must not match "Inowrocław" or "Wrocławskie"
364            // pointCityTokens extracts city from last comma-segment of p.a (all countries).
365            // pointAddrTokens is broader fallback — all address tokens (handles unusual formats).
366            cityPoints = inCountry.filter(function (p) {
367                return tokensMatchAllExact(parsed.cityTokens, pointCityTokens(p)) ||
368                       tokensMatchAllExact(parsed.cityTokens, pointAddrTokens(p));
369            });
370            // If no exact matches (partial query like "Wroc"), fall back to substring
371            if (cityPoints.length === 0) {
372                cityPoints = inCountry.filter(function (p) {
373                    return tokensMatchAll(parsed.cityTokens, pointCityTokens(p)) ||
374                           tokensMatchAll(parsed.cityTokens, pointAddrTokens(p));
375                });
376            }
377        }
378
379        var streetPoints = [];
380        if (parsed.streetTokens.length > 0 && cityPoints.length > 0) {
381            streetPoints = cityPoints.filter(function (p) {
382                return tokensMatchAll(parsed.streetTokens, pointStreetTokens(p));
383            });
384        }
385
386        var hasLocker = !!lockerPoint;
387        var hasPostal = postalPoints.length > 0;
388        var hasCity   = cityPoints.length > 0;
389        var hasStreet = streetPoints.length > 0;
390
391        var type = 'FALLBACK';
392        if (hasLocker)                type = 'LOCKER_CODE_EXACT';
393        else if (hasPostal && hasCity) type = 'CITY_POSTAL';
394        else if (hasPostal)           type = 'POSTAL_ONLY';
395        else if (hasCity && hasStreet) type = 'CITY_STREET';
396        else if (hasCity)             type = 'CITY_ONLY';
397        else if (parsed.streetTokens.length > 0) type = 'STREET_ONLY';
398
399        var cityMatchesPostal = true;
400        if (parsed.postal && hasPostal && hasCity) {
401            var postalCities = postalPoints
402                .map(function (p) { return pointCityTokens(p).join(' '); })
403                .filter(function (x) { return x.length > 0; });
404            var citySet = {};
405            postalCities.forEach(function (c) { citySet[c] = true; });
406            var parsedCityAscii = normalizeDiacritics(parsed.cityTokens.join(' '));
407            cityMatchesPostal = Object.keys(citySet).some(function (c) {
408                return normalizeDiacritics(c).indexOf(parsedCityAscii) !== -1;
409            });
410        }
411
412        var streetExistsInCityOrPostal = true;
413        if (parsed.streetTokens.length > 0) {
414            var base = hasCity ? cityPoints : postalPoints;
415            if (base.length > 0) {
416                streetExistsInCityOrPostal = base.some(function (p) {
417                    return tokensMatchAll(parsed.streetTokens, pointStreetTokens(p));
418                });
419            }
420        }
421
422        return {
423            inCountry: inCountry,
424            lockerPoint: lockerPoint,
425            postalPoints: postalPoints,
426            cityPoints: cityPoints,
427            streetPoints: streetPoints,
428            type: type,
429            consistency: {
430                cityMatchesPostal: cityMatchesPostal,
431                streetExistsInCityOrPostal: streetExistsInCityOrPostal
432            }
433        };
434    }
435
436    // ── Relevance scoring ─────────────────────────────────────────────────────
437
438    function sortByRelevance(points, parsed, countryCode) {
439        var cityTokens   = parsed.cityTokens;
440        var streetTokens = parsed.streetTokens;
441        var postalNorm   = parsed.postal ? parsed.postal.normalized : null;
442        var queryTokens  = tokensFrom(parsed.normalized);
443
444        function scorePoint(p) {
445            var s = 0;
446            var name    = normalizeDiacritics(normalizeText(p.n || ''));
447            var cityT   = pointCityTokens(p);
448            var streetT = pointStreetTokens(p);
449            var postal  = pointPostal(p, countryCode);
450
451            if (postalNorm && postal === postalNorm)                                    s += 400;
452            if (cityTokens.length > 0 && tokensMatchAll(cityTokens, cityT))             s += 300;
453            if (streetTokens.length > 0 && tokensMatchAll(streetTokens, streetT))       s += 200;
454            if (streetTokens.length > 0 && tokensMatchAny(streetTokens, streetT))       s += 50;
455            if (parsed.lockerId && name.indexOf(parsed.lockerId.toLowerCase()) !== -1)  s += 500;
456            if (tokensMatchAny(queryTokens, cityT))                                      s += 250;
457
458            // Penalize test/dummy entries (contain "TEST" or are purely sequential numbers)
459            if (/\btest\b/i.test(p.n || '')) { s -= 1000; }
460
461            // Penalize points that don't match the searched street when street was specified
462            // Makes "Warszawa ul. Powstańców Śląskich" not show unrelated street points first
463            if (streetTokens.length > 0 && !tokensMatchAny(streetTokens, streetT)) { s -= 350; }
464
465            return s;
466        }
467
468        return points
469            .map(function (p) { return { p: p, s: scorePoint(p) }; })
470            .sort(function (a, b) { return b.s - a.s; })
471            .map(function (x) { return x.p; });
472    }
473
474    function pointHasBuildingNumber(point, buildingNumber) {
475        if (!buildingNumber) return false;
476        return (point.a || '').toLowerCase().indexOf(String(buildingNumber).toLowerCase()) !== -1;
477    }
478
479    // ── Fallback broad filter ─────────────────────────────────────────────────
480
481    var NOISE_SHORT_NUM = /^\d{1,2}$/;
482
483    function buildFallbackFilter(points, countryCode, parsed) {
484        var inCountry = filterByCountry(points, countryCode);
485        var qTokens = tokensFrom(parsed.normalized);
486        var cityQ = qTokens.filter(function (t) {
487            return t && !NOISE_SHORT_NUM.test(t) && !NOISE_WORDS_COMMON[t] && t.length >= 3;
488        });
489        return inCountry.filter(function (p) {
490            var name = tokensFrom(normalizeDiacritics(normalizeText(p.n || '')));
491            var ct   = pointCityTokens(p);
492            var at   = pointAddrTokens(p);   // full address — city may be here for UK/LT etc.
493            var st   = pointStreetTokens(p);
494            // City filter uses prefix/exact matching (NOT substring) to prevent
495            // "gora" matching "bilgoraj" or "nowa" matching "nowy" etc.
496            // Also check full address tokens for countries where p.l doesn't have city.
497            if (cityQ.length) {
498                var _allCityT = ct.concat(at);
499                var _cityOk = cityQ.every(function(qt) {
500                    return _allCityT.some(function(tt) { return tt === qt || tt.indexOf(qt) === 0; });
501                });
502                if (!_cityOk) return false;
503            }
504            return tokensMatchAny(qTokens, name) || tokensMatchAny(qTokens, ct) || tokensMatchAny(qTokens, st) || tokensMatchAny(qTokens, at);
505        });
506    }
507
508    // ── Main search ───────────────────────────────────────────────────────────
509
510    function search(points, rawQuery, options) {
511        var opts        = options || {};
512        var countryCode = opts.countryCode || null;
513        var sanitized   = sanitizeQuery(rawQuery);
514
515        if (!sanitized) {
516            return { results: [], meta: { type: 'EMPTY', intent: null, conflict: false } };
517        }
518
519        var parsed  = parseQuery(sanitized, countryCode);
520        var compact = parsed.normalized.replace(/\s+/g, '');
521
522        // Reject pure-numeric strings too short to be a postal code
523        if (
524            compact && /^\d+$/.test(compact) &&
525            (!parsed.postal || !parsed.postal.normalized) &&
526            compact.length < 4 &&
527            parsed.cityTokens.length === 0 &&
528            parsed.streetTokens.length === 0 &&
529            !parsed.lockerId
530        ) {
531            return { results: [], meta: { type: 'NUMERIC_TOO_SHORT', intent: null, conflict: false } };
532        }
533
534        var intent     = evaluateIntent(parsed, points, countryCode);
535        var results    = [];
536        var searchType = intent.type;
537
538        if (intent.type === 'LOCKER_CODE_EXACT' && intent.lockerPoint) {
539            // Locker ID gets priority slot #1, then fill with context-based results.
540            // This allows queries like "Warszawa KRA01B" to return the exact locker
541            // FIRST (with yellow pulse in UI) + nearby Warsaw points after it.
542            var _otherParsed = {
543                normalized: parsed.normalized,
544                cityTokens: parsed.cityTokens,
545                streetTokens: parsed.streetTokens,
546                postal: parsed.postal,
547                lockerId: null,
548                buildingNumber: parsed.buildingNumber
549            };
550            var _others = sortByRelevance(
551                buildFallbackFilter(points, countryCode, _otherParsed).filter(function(p) {
552                    return p !== intent.lockerPoint;
553                }),
554                _otherParsed, countryCode
555            );
556            results = [intent.lockerPoint].concat(_others);
557        } else if (intent.type === 'CITY_POSTAL') {
558            // PERF: Set lookup O(1) per element instead of indexOf O(M) — critical for large cities
559            var _citySet = new Set(intent.cityPoints);
560            var intersection = intent.postalPoints.filter(function (p) {
561                return _citySet.has(p);
562            });
563            results = sortByRelevance(
564                intersection.length > 0 ? intersection : intent.postalPoints,
565                parsed, countryCode
566            );
567        } else if (intent.type === 'POSTAL_ONLY') {
568            results = sortByRelevance(intent.postalPoints, parsed, countryCode);
569        } else if (intent.type === 'CITY_STREET') {
570            var base = intent.streetPoints.length > 0 ? intent.streetPoints : intent.cityPoints;
571            if (parsed.buildingNumber && base.length > 0) {
572                var narrowed = base.filter(function (p) {
573                    return pointHasBuildingNumber(p, parsed.buildingNumber);
574                });
575                if (narrowed.length > 0) base = narrowed;
576            }
577            results = sortByRelevance(base, parsed, countryCode);
578        } else if (intent.type === 'CITY_ONLY') {
579            results = sortByRelevance(intent.cityPoints, parsed, countryCode);
580        } else if (intent.type === 'STREET_ONLY') {
581            var inC = filterByCountry(points, countryCode);
582            results = sortByRelevance(
583                inC.filter(function (p) { return tokensMatchAll(parsed.streetTokens, pointStreetTokens(p)); }),
584                parsed, countryCode
585            );
586        } else {
587            results = sortByRelevance(buildFallbackFilter(points, countryCode, parsed), parsed, countryCode);
588            searchType = 'FALLBACK';
589        }
590
591        // Second-pass fallback if nothing found
592        if (!results.length) {
593            results = sortByRelevance(buildFallbackFilter(points, countryCode, parsed), parsed, countryCode);
594            searchType = 'FALLBACK';
595        }
596
597        var maxResults = typeof opts.limit === 'number' ? opts.limit : 500;
598
599        // ── Carrier interleaving for city/street searches ─────────────────────────
600        // Problem: a single carrier (e.g. Orlen) may dominate results for a city,
601        // pushing all other carriers far down. Interleave by carrier so results
602        // show variety: InPost, DPD, Orlen, Pocztex, … mixed together.
603        //
604        // Only apply to multi-carrier situations. Top N results (street-specific
605        // or first best match) are preserved; the remaining are interleaved.
606        if (searchType === 'CITY_ONLY' || searchType === 'CITY_STREET' || searchType === 'CITY_POSTAL') {
607            var _seenCarriers = {};
608            results.forEach(function(p) { _seenCarriers[p.carrier || 'inpost'] = true; });
609            var _numCarriers = Object.keys(_seenCarriers).length;
610            if (_numCarriers >= 2) {
611                // All results go through carrier interleaving (no top-tier bypass)
612                var _topTier  = [];
613                var _restTier = results;
614                // Group by carrier
615                var _grp = {}, _gOrd = [];
616                _restTier.forEach(function(p) {
617                    var c = p.carrier || 'inpost';
618                    if (!_grp[c]) { _grp[c] = []; _gOrd.push(c); }
619                    _grp[c].push(p);
620                });
621                // Round-robin
622                var _il = [];
623                var _mL = 0;
624                _gOrd.forEach(function(c) { _mL = Math.max(_mL, _grp[c].length); });
625                for (var _ri = 0; _ri < _mL; _ri++) {
626                    _gOrd.forEach(function(c) {
627                        if (_grp[c][_ri]) { _il.push(_grp[c][_ri]); }
628                    });
629                }
630                results = _topTier.concat(_il);
631            }
632        }
633
634        if (results.length > maxResults) results = results.slice(0, maxResults);
635
636        var conflict = intent.consistency && (
637            !intent.consistency.cityMatchesPostal ||
638            !intent.consistency.streetExistsInCityOrPostal
639        );
640
641        return {
642            results: results,
643            meta: { type: searchType, intent: intent, conflict: conflict }
644        };
645    }
646
647    // PERF: pre-compute expensive per-point fields once after data loads.
648    // Chunked via requestIdleCallback to avoid blocking main thread on 100k+ points.
649    // Country switch: each load builds a FRESH points array (anunaki-popup.js allPoints), so a
650    // stale _postal never rides on new objects. The cancellation token (_pcToken) stops a pending
651    // idle-chain from a previous country from continuing to write into an abandoned array.
652    SearchEngine.precompute = function(points, countryCode) {
653        var index = 0;
654        var CHUNK = 1000;
655        var myToken = (SearchEngine._pcToken = (SearchEngine._pcToken || 0) + 1);
656
657        function processChunk(deadline) {
658            if (myToken !== SearchEngine._pcToken) { return; } // superseded by a newer precompute (country switch)
659            var timeOk = !deadline || deadline.timeRemaining() > 2;
660            while (index < points.length && timeOk) {
661                var p = points[index++];
662                p._postal       = pointPostal(p, countryCode);
663                p._cityTokens   = pointCityTokens(p);
664                p._addrTokens   = pointAddrTokens(p);
665                p._streetTokens = pointStreetTokens(p);
666                if (deadline && index % CHUNK === 0) {
667                    timeOk = deadline.timeRemaining() > 2;
668                }
669            }
670            if (index < points.length) {
671                if (typeof requestIdleCallback !== 'undefined') {
672                    requestIdleCallback(processChunk, { timeout: 500 });
673                } else {
674                    setTimeout(function() { processChunk(null); }, 0);
675                }
676            }
677        }
678
679        if (typeof requestIdleCallback !== 'undefined') {
680            requestIdleCallback(processChunk, { timeout: 500 });
681        } else {
682            setTimeout(function() { processChunk(null); }, 0);
683        }
684    };
685
686    SearchEngine.search         = search;
687    SearchEngine.parseQuery     = parseQuery;
688    SearchEngine.evaluateIntent = evaluateIntent;
689    SearchEngine.VERSION        = 2;
690
691    window.KasamukaSearchEngine = SearchEngine;
692
693})(window);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.