PageSourceSearch

https://www.mirela.bg/js/components/search-bar-head.bundle.js?v=1790088876

js mirela.bg collected 2026-09-24 18:47:58 UTC 391,789 bytes, 8,739 lines download raw bytes

1/* Auto-generated by scripts/build-search-bar-components-bundle.mjs. Do not edit manually. */
2
3// ---- START: js/components/mirela-nb-seo-slugs-runtime.js ----
4/**
5 * New-buildings list SEO city-segment helpers. Prefer window.__mirelaNbSeoCitySlugs from SSR (search-bar-scripts).
6 */
7(function (window) {
8    'use strict';
9
10    var FALLBACK = ['novi-sgradi-v-stroej', 'new-buildings-under-construction', 'novye-zdaniya-v-stroitelstve', 'neubauten-im-bau'];
11
12    function slugSet() {
13        try {
14            var s = window.__mirelaNbSeoCitySlugs;
15            if (Array.isArray(s) && s.length) return s.map(function (x) { return String(x || '').toLowerCase().trim(); }).filter(Boolean);
16        } catch (_e) {}
17        return FALLBACK.slice();
18    }
19
20    if (typeof window.__mirelaIsNbSeoCitySlug !== 'function') {
21        window.__mirelaIsNbSeoCitySlug = function (seg) {
22            var x = String(seg == null ? '' : seg).toLowerCase().trim();
23            if (!x) return false;
24            var a = slugSet();
25            return a.indexOf(x) !== -1;
26        };
27    }
28
29    if (typeof window.__mirelaPathnameHasNbSeoCity !== 'function') {
30        window.__mirelaPathnameHasNbSeoCity = function (pathname) {
31            try {
32                var ru = window.MirelaSearch && window.MirelaSearch.routeUtils;
33                var parts = (ru && typeof ru.getPathSegments === 'function')
34                    ? ru.getPathSegments(pathname || '')
35                    : String(pathname || '').replace(/^\/+/, '').split('/').filter(Boolean);
36                var hasLoc = !!(parts.length && ru && typeof ru.isLocaleSegment === 'function' && ru.isLocaleSegment(parts[0]));
37                var dealIdx = hasLoc ? 1 : 0;
38                var citySeg = String(parts[dealIdx + 1] || '').trim();
39                return window.__mirelaIsNbSeoCitySlug(citySeg);
40            } catch (_e2) {
41                return false;
42            }
43        };
44    }
45})(window);
46
47// ---- END: js/components/mirela-nb-seo-slugs-runtime.js ----
48
49// ---- START: js/components/mirela-search-legacy-bridge.js ----
50/* Legacy global bridge for search helpers. */
51(function (window, document) {
52    'use strict';
53
54    var root = window.MirelaSearch = window.MirelaSearch || {};
55    var legacy = root.legacy = root.legacy || {};
56
57    function getSharedApi() {
58        try { return window.AdvancedSearchShared || null; } catch (_e) { return null; }
59    }
60
61    function getFromShared(methodName) {
62        var shared = getSharedApi();
63        if (shared && typeof shared[methodName] === 'function') return shared[methodName].bind(shared);
64        return null;
65    }
66
67    legacy.getSharedApi = function () { return getSharedApi(); };
68
69    legacy.q = function (selector) {
70        try {
71            var fn = getFromShared('selectorGet');
72            if (fn) return fn(selector);
73        } catch (_e) {}
74        try { return document.querySelector(selector); } catch (_e2) { return null; }
75    };
76
77    legacy.ribbonFilterCapture = function (fromInp, toInp, sanitize) {
78        try {
79            var fn = getFromShared('rangeCapture');
80            if (fn) return fn(fromInp, toInp, sanitize);
81        } catch (_e) {}
82        return {
83            from: String((sanitize && sanitize(fromInp && fromInp.value)) || ''),
84            to: String((sanitize && sanitize(toInp && toInp.value)) || '')
85        };
86    };
87
88    legacy.normalizeRibbonRange = function (opts) {
89        try {
90            var fn = getFromShared('rangeNormalize');
91            if (fn) return fn(opts || {});
92        } catch (_e) {}
93        opts = opts || {};
94        var fromInp = opts.fromInp || null;
95        var toInp = opts.toInp || null;
96        var sanitize = opts.sanitize;
97        var mirrorTo = Array.isArray(opts.mirrorToInputs) ? opts.mirrorToInputs : [];
98        var minGap = opts.minGap;
99        function valOf(inp) {
100            try {
101                if (sanitize) return sanitize((inp && inp.value) || '');
102                return ((inp && inp.value) || '').toString().trim();
103            } catch (_e2) { return ''; }
104        }
105        function setMirrorToInputs(str) {
106            mirrorTo.forEach(function (inp) { try { if (inp) inp.value = str; } catch (_e4) {} });
107        }
108        var f = valOf(fromInp);
109        var t = valOf(toInp);
110        if (!f || !t) {
111            return { from: String(f || ''), to: String(t || ''), changed: false };
112        }
113        var fnn = parseInt(String(f || ''), 10) || 0;
114        var tnn = parseInt(String(t || ''), 10) || 0;
115        var narrow = false;
116        try {
117            narrow = typeof window !== 'undefined' && window.innerWidth < 1050;
118        } catch (_eW) {}
119        if (typeof minGap === 'number' && minGap >= 0) {
120            if (narrow) {
121                if (tnn < fnn) {
122                    try { if (toInp) toInp.value = ''; } catch (_e3) {}
123                    setMirrorToInputs('');
124                    return { from: String(f || ''), to: '', changed: true };
125                }
126            } else if (tnn < fnn + minGap) {
127                var bumped = fnn + minGap;
128                var bumpedStr = String(bumped);
129                try { if (toInp) toInp.value = bumpedStr; } catch (_e3b) {}
130                setMirrorToInputs(bumpedStr);
131                return { from: String(f || ''), to: bumpedStr, changed: true };
132            }
133        } else if (tnn < fnn) {
134            try { if (toInp) toInp.value = ''; } catch (_e3c) {}
135            setMirrorToInputs('');
136            return { from: String(f || ''), to: '', changed: true };
137        }
138        return { from: String(f || ''), to: String(t || ''), changed: false };
139    };
140
141    legacy.ribbonFilterOnOk = function (opts) {
142        try {
143            var fn = getFromShared('rangeOnOk');
144            if (fn) return fn(opts || {});
145        } catch (_e) {}
146        opts = opts || {};
147        var fromInp = opts.fromInp, toInp = opts.toInp, sanitize = opts.sanitize;
148        var storedFrom = opts.storedFrom, storedTo = opts.storedTo;
149        var syncBeforeSubmit = opts.syncBeforeSubmit, submit = opts.submit, closeDd = opts.closeDd;
150        var curFrom = String((sanitize && sanitize(fromInp && fromInp.value)) || '');
151        var curTo = String((sanitize && sanitize(toInp && toInp.value)) || '');
152        if (curFrom === storedFrom && curTo === storedTo) { if (closeDd) closeDd(); return false; }
153        if (typeof syncBeforeSubmit === 'function') syncBeforeSubmit();
154        if (typeof submit === 'function') submit();
155        if (closeDd) closeDd();
156        return true;
157    };
158
159    legacy.canAjaxList = function () {
160        try {
161            var fn = getFromShared('canAjaxList');
162            if (fn) return !!fn();
163        } catch (_e) {}
164        try { if (window.__disableAjaxList) return false; } catch (_e2) {}
165        return !!(legacy.q('.wrap-left') && typeof window.loadOffers === 'function');
166    };
167
168    legacy.persistFilterState = function () {
169        try {
170            var fn = getFromShared('persistFilterState');
171            if (fn) { fn(); return; }
172        } catch (_e) {}
173        try {
174            var persist = window.__mirelaPersistLastListContext || window.__mirelaPersistListFiltersState;
175            if (typeof persist === 'function') persist();
176        } catch (_e3) {}
177    };
178
179    legacy.refreshAdvancedCounter = function (delayMs) {
180        try {
181            var fn = getFromShared('refreshCounterDelayed');
182            if (fn) { fn(delayMs); return; }
183        } catch (_e) {}
184        var ms = (typeof delayMs === 'number') ? delayMs : 50;
185        setTimeout(function () {
186            try { if (typeof window.updateAdvancedFiltersCount === 'function') window.updateAdvancedFiltersCount(); } catch (_e2) {}
187        }, ms);
188    };
189
190    legacy.submitCurrentSearch = function (opts) {
191        try {
192            var fn = getFromShared('submitCurrentSearch');
193            if (fn) return !!fn(opts || {});
194        } catch (_e) {}
195        opts = opts || {};
196        var form = opts.form || legacy.q('.js-search-form');
197        if (!form) return false;
198        try { window.__ribbonOkSubmit = true; } catch (_e2) {}
199        try {
200            var requestPolicy = window.MirelaSearch && window.MirelaSearch.requestPolicy;
201            if (requestPolicy && typeof requestPolicy.runSearchAction === 'function') {
202                return !!requestPolicy.runSearchAction('search_button', { form: form, force: true });
203            }
204            if (typeof form.requestSubmit === 'function') form.requestSubmit();
205            else form.submit();
206        } finally {
207            try { window.__ribbonOkSubmit = false; } catch (_e3) {}
208        }
209        return true;
210    };
211
212    /**
213     * Home / shared search bar: must use jQuery submit event with isProgrammatic set.
214     * Native form.dispatchEvent(new Event('submit')) often does not surface isProgrammatic to
215     * $('.js-search-form').on('submit', …) the way home.js expects → falls through to buildHomeListUrl.
216     */
217    legacy.dispatchProgrammaticSearchSubmit = function (form) {
218        form = form || legacy.q('.js-search-form');
219        if (!form) return;
220        var usedJq = false;
221        try { window.__ribbonOkSubmit = true; } catch (_e0) {}
222        try {
223            var $ = window.jQuery || window.$;
224            if ($ && $.fn && typeof $.fn.trigger === 'function') {
225                var jqEv = $.Event('submit', { cancelable: true, bubbles: true });
226                jqEv.isProgrammatic = true;
227                $(form).trigger(jqEv);
228                usedJq = true;
229            }
230        } catch (_e1) {}
231        if (!usedJq) {
232            try {
233                var ev = new Event('submit', { cancelable: true, bubbles: true });
234                ev.isProgrammatic = true;
235                form.dispatchEvent(ev);
236            } catch (_e2) {}
237        }
238        try { window.__ribbonOkSubmit = false; } catch (_e3) {}
239    };
240
241    legacy.rangeCloseOpposite = function (opts) {
242        try { var fn = getFromShared('rangeCloseOpposite'); if (fn) return fn(opts || {}); } catch (_e) {}
243    };
244    legacy.queueRangeSubmit = function (form) {
245        try { var fn = getFromShared('rangeQueueSubmit'); if (fn) return fn(form); } catch (_e) {}
246    };
247    legacy.handleRangeClear = function (opts) {
248        try { var fn = getFromShared('rangeHandleClear'); if (fn) return fn(opts || {}); } catch (_e) {}
249    };
250    legacy.bindRangeInputShield = function (fromInp, toInp) {
251        try { var fn = getFromShared('bindRangeInputShield'); if (fn) return fn(fromInp, toInp); } catch (_e) {}
252    };
253    legacy.prefillRangeFromSearchBar = function (opts) {
254        try { var fn = getFromShared('prefillRangeFromSearchBar'); if (fn) return fn(opts || {}); } catch (_e) {}
255    };
256    legacy.bindAdvancedRangeMirrors = function (opts) {
257        try { var fn = getFromShared('bindAdvancedRangeMirrors'); if (fn) return fn(opts || {}); } catch (_e) {}
258    };
259    legacy.bindRangeSubmitSync = function (opts) {
260        try { var fn = getFromShared('bindRangeSubmitSync'); if (fn) return fn(opts || {}); } catch (_e) {}
261    };
262    legacy.bindRangeOutsideClose = function (opts) {
263        try { var fn = getFromShared('bindRangeOutsideClose'); if (fn) return fn(opts || {}); } catch (_e) {}
264        return function () {};
265    };
266    legacy.bindRangeBlurCommit = function (opts) {
267        try { var fn = getFromShared('bindRangeBlurCommit'); if (fn) return fn(opts || {}); } catch (_e) {}
268    };
269    legacy.createRangeListAdapter = function (opts) {
270        try { var fn = getFromShared('createRangeListAdapter'); if (fn) return fn(opts || {}); } catch (_e) {}
271        return null;
272    };
273
274    // Backward-compatible globals (delegating to namespaced legacy bridge).
275    window.__mirelaGetSharedApi = legacy.getSharedApi;
276    window.__mirelaQ = legacy.q;
277    window.__mirelaRibbonFilterCapture = legacy.ribbonFilterCapture;
278    window.__mirelaNormalizeRibbonRange = legacy.normalizeRibbonRange;
279    window.__mirelaRibbonFilterOnOk = legacy.ribbonFilterOnOk;
280    window.__mirelaCanAjaxList = legacy.canAjaxList;
281    window.__mirelaPersistFilterState = legacy.persistFilterState;
282    window.__mirelaRefreshAdvancedCounter = legacy.refreshAdvancedCounter;
283    window.__mirelaSubmitCurrentSearch = legacy.submitCurrentSearch;
284    window.__mirelaDispatchProgrammaticSearchSubmit = legacy.dispatchProgrammaticSearchSubmit;
285    window.__mirelaRangeCloseOpposite = legacy.rangeCloseOpposite;
286    window.__mirelaQueueRangeSubmit = legacy.queueRangeSubmit;
287    window.__mirelaHandleRangeClear = legacy.handleRangeClear;
288    window.__mirelaBindRangeInputShield = legacy.bindRangeInputShield;
289    window.__mirelaPrefillRangeFromSearchBar = legacy.prefillRangeFromSearchBar;
290    window.__mirelaBindAdvancedRangeMirrors = legacy.bindAdvancedRangeMirrors;
291    window.__mirelaBindRangeSubmitSync = legacy.bindRangeSubmitSync;
292    window.__mirelaBindRangeOutsideClose = legacy.bindRangeOutsideClose;
293    window.__mirelaBindRangeBlurCommit = legacy.bindRangeBlurCommit;
294    window.__mirelaCreateRangeListAdapter = legacy.createRangeListAdapter;
295})(window, document);
296
297
298// ---- END: js/components/mirela-search-legacy-bridge.js ----
299
300// ---- START: js/components/search-route-utils.js ----
301/* Shared route/deal/locale helpers for search flows. */
302(function (window) {
303    'use strict';
304
305    var root = window.MirelaSearch = window.MirelaSearch || {};
306    var api = root.routeUtils = root.routeUtils || {};
307
308    var DEAL_SEGMENT_RE = '(?:prodazhbi|naemi|for-sale|for-rent|prodazha|arenda|verkauf|miete|kupuvachi|naemateli)';
309    var DEAL_SEGMENT_EXACT_RE = new RegExp('^' + DEAL_SEGMENT_RE + '$', 'i');
310    var LIST_ROUTE_RE = new RegExp('^/(?:en|de|ru)?/?' + DEAL_SEGMENT_RE + '(?:/|$)', 'i');
311    var OFFER_LIKE_PATH_RE = new RegExp('/' + DEAL_SEGMENT_RE + '/[^/]+/[^/]+-\\d+(?:\\.html)?$', 'i');
312    var DEAL_SEGMENT_PATH_RE = new RegExp('/' + DEAL_SEGMENT_RE + '/', 'i');
313
314    function getPathSegments(pathname) {
315        return String(pathname || '').split('/').filter(Boolean);
316    }
317
318    function isLocaleSegment(seg) {
319        var s = String(seg || '').toLowerCase();
320        return (s === 'bg' || s === 'en' || s === 'ru' || s === 'de');
321    }
322
323    function detectLocaleFromPath(pathname) {
324        try {
325            var first = getPathSegments(pathname || '')[0] || '';
326            return isLocaleSegment(first) ? String(first).toLowerCase() : 'bg';
327        } catch (_e) {
328            return 'bg';
329        }
330    }
331
332    function getLocalePrefix(pathname, opts) {
333        try {
334            var o = opts || {};
335            var includeBg = !!o.includeBg;
336            var locale = detectLocaleFromPath(pathname || window.location.pathname || '');
337            if (locale === 'bg' && !includeBg) return '';
338            return '/' + locale;
339        } catch (_e) {
340            return '';
341        }
342    }
343
344    function isDealSegment(seg) {
345        return DEAL_SEGMENT_EXACT_RE.test(String(seg || ''));
346    }
347
348    function isListRoutePath(pathname) {
349        try {
350            var p = String(pathname || '').toLowerCase();
351            return LIST_ROUTE_RE.test(p);
352        } catch (_e) {
353            return false;
354        }
355    }
356
357    function isOfferLikePath(pathname) {
358        try {
359            var p = String(pathname || '').trim();
360            if (!p) return false;
361            if (/\/offer\/\d+(?:\/)?$/i.test(p)) return true;
362            return OFFER_LIKE_PATH_RE.test(p);
363        } catch (_e) {
364            return false;
365        }
366    }
367
368    function extractOfferIdFromPath(pathname) {
369        try {
370            var path = String(pathname || '').trim();
371            if (!path) return '';
372            var offerPathMatch = path.match(/\/offer\/(\d+)(?:\/)?$/i);
373            if (offerPathMatch) return String(offerPathMatch[1] || '').trim();
374            if (!isOfferLikePath(path)) return '';
375            var slugPathMatch = path.match(/-(\d+)(?:\.html)?$/i);
376            return slugPathMatch ? String(slugPathMatch[1] || '').trim() : '';
377        } catch (_e) {
378            return '';
379        }
380    }
381
382    function buildOfferRedirectUrl(offerId, pathname) {
383        var id = String(offerId || '').trim();
384        var safeId = encodeURIComponent(id);
385        if (!safeId) return '/offer/';
386        return getLocalePrefix(pathname || window.location.pathname || '') + '/offer/' + safeId;
387    }
388
389    function getDealSegmentIndex(pathParts) {
390        var parts = Array.isArray(pathParts) ? pathParts : [];
391        if (!parts.length) return 0;
392        return isLocaleSegment(parts[0]) ? 1 : 0;
393    }
394
395    function containsDealSegmentPath(hrefOrPath) {
396        return DEAL_SEGMENT_PATH_RE.test(String(hrefOrPath || ''));
397    }
398
399    /**
400     * Preset accent list URLs: /{deal}/{slug}-offer{ID} — filters may exist only server-side.
401     * Used by URL authority / hydrator to avoid stripping SSR preset facets.
402     */
403    function isPresetAccentListPath(pathname) {
404        try {
405            var path = String(pathname || '').trim();
406            return /^\/(?:en\/|ru\/|de\/)?(?:prodazhbi|naemi|for-sale|for-rent|prodazha|arenda|verkauf|miete)\/[^/]+-offer\d+(?:\.html)?$/i.test(path);
407        } catch (_e) {
408            return false;
409        }
410    }
411
412    /** True when path is only locale (optional) + deal segment — no city/preset slug after the deal. */
413    function isCollapsedDealOnlyListPath(pathname) {
414        var parts = getPathSegments(pathname);
415        if (!parts.length) return false;
416        var i = getDealSegmentIndex(parts);
417        if (!isDealSegment(parts[i] || '')) return false;
418        return parts.length <= i + 1;
419    }
420
421    /**
422     * When replaceState collapses /{deal}/{slug}-offer{id} to /{deal}?page=…, restore the accent pathname
423     * so __preset_id and URL sync stay aligned with the pinned preset list session.
424     */
425    function resolvePresetAccentPathnameForList(pathname) {
426        var path = String(pathname != null ? pathname : '').trim();
427        if (!path && typeof window !== 'undefined') {
428            path = String(window.location.pathname || '');
429        }
430        if (isPresetAccentListPath(path)) return path;
431        var pinned = '';
432        if (typeof window !== 'undefined' && window.__mirelaPinnedPresetListPathname) {
433            pinned = String(window.__mirelaPinnedPresetListPathname || '').trim();
434        }
435        if (!pinned || !isPresetAccentListPath(pinned)) return path;
436        if (!isCollapsedDealOnlyListPath(path)) return path;
437        return pinned;
438    }
439
440    /** Clear pinned preset when user navigates to a non-accent list path (e.g. /{deal}/grad-…). */
441    function shouldClearPresetAccentPin(pathname) {
442        var parts = getPathSegments(pathname);
443        var i = getDealSegmentIndex(parts);
444        if (parts.length <= i + 1) return false;
445        var seg = String(parts[i + 1] || '');
446        if (/-offer\d/i.test(seg)) return false;
447        return true;
448    }
449
450    api.getPathSegments = getPathSegments;
451    api.isLocaleSegment = isLocaleSegment;
452    api.detectLocaleFromPath = detectLocaleFromPath;
453    api.getLocalePrefix = getLocalePrefix;
454    api.isDealSegment = isDealSegment;
455    api.isListRoutePath = isListRoutePath;
456    api.isOfferLikePath = isOfferLikePath;
457    api.extractOfferIdFromPath = extractOfferIdFromPath;
458    api.buildOfferRedirectUrl = buildOfferRedirectUrl;
459    api.getDealSegmentIndex = getDealSegmentIndex;
460    api.containsDealSegmentPath = containsDealSegmentPath;
461    api.isPresetAccentListPath = isPresetAccentListPath;
462    api.isCollapsedDealOnlyListPath = isCollapsedDealOnlyListPath;
463    api.resolvePresetAccentPathnameForList = resolvePresetAccentPathnameForList;
464    api.shouldClearPresetAccentPin = shouldClearPresetAccentPin;
465})(window);
466
467// ---- END: js/components/search-route-utils.js ----
468
469// ---- START: js/components/search-canonical-utils.js ----
470/* Shared canonical URL cleanup helpers for list/search pages. */
471(function (window) {
472    'use strict';
473
474    var root = window.MirelaSearch = window.MirelaSearch || {};
475    var api = root.canonicalUtils = root.canonicalUtils || {};
476
477    function getRouteUtils() {
478        try {
479            return (window.MirelaSearch && window.MirelaSearch.routeUtils) ? window.MirelaSearch.routeUtils : null;
480        } catch (_e) {
481            return null;
482        }
483    }
484
485    function getPathSegments(pathLike) {
486        var raw = String(pathLike || '').trim();
487        if (!raw) return [];
488        try {
489            var u = new URL(raw, window.location.origin);
490            raw = String(u.pathname || '');
491        } catch (_eUrl) {
492            raw = raw.split('#')[0].split('?')[0];
493        }
494        return raw.replace(/^\/+/, '').split('/').filter(Boolean);
495    }
496
497    function isLocaleSegment(seg, opts) {
498        try {
499            if (opts && typeof opts.isLocaleSegment === 'function') {
500                return !!opts.isLocaleSegment(seg);
501            }
502        } catch (_e0) {}
503        try {
504            var route = getRouteUtils();
505            if (route && typeof route.isLocaleSegment === 'function') {
506                return !!route.isLocaleSegment(seg);
507            }
508        } catch (_e1) {}
509        var s = String(seg || '').toLowerCase();
510        return (s === 'bg' || s === 'en' || s === 'ru' || s === 'de');
511    }
512
513    function toUniqueValues(values) {
514        var out = [];
515        var seen = {};
516        (values || []).forEach(function (v) {
517            var s = String(v || '').trim();
518            if (!s) return;
519            if (s.indexOf(',') !== -1) {
520                s.split(',').forEach(function (x) {
521                    var sx = String(x || '').trim();
522                    if (!sx || seen[sx]) return;
523                    seen[sx] = true;
524                    out.push(sx);
525                });
526                return;
527            }
528            if (seen[s]) return;
529            seen[s] = true;
530            out.push(s);
531        });
532        return out;
533    }
534
535    function isLegacyStreetPathSegment(seg) {
536        return /^street-\d+$/i.test(String(seg || '').trim());
537    }
538
539    function isStreetSeoPrefixSegment(seg) {
540        var s = String(seg || '').trim().toLowerCase();
541        return s === 'street' || s === 'ulitsa' || s === 'strasse'
542            || s === 'kompleks' || s === 'complex' || s === 'komplex';
543    }
544
545    function isStreetSeoSlugSegment(seg) {
546        return /.+-\d+$/i.test(String(seg || '').trim());
547    }
548
549    function cleanupPathEncodedFilters(params, opts) {
550        var usp = (params instanceof URLSearchParams) ? params : new URLSearchParams();
551        var o = opts || {};
552        try {
553            var bp = String((o.basePath == null ? '' : o.basePath) || '').trim();
554            if (!bp) bp = String(window.location.pathname || '');
555
556            var pathParts = getPathSegments(bp);
557            var firstSeg = String(pathParts[0] || '').toLowerCase();
558            var hasLocale = isLocaleSegment(firstSeg, o);
559            var cityIdx = hasLocale ? 2 : 1;
560            var segAfterCity = String(pathParts[cityIdx + 1] || '').trim();
561            var segAfterDistrict = String(pathParts[cityIdx + 2] || '').trim();
562
563            if (pathParts[cityIdx]) {
564                usp.delete('city_id');
565                usp.delete('city_id[]');
566            }
567
568            var isLegacyStreetPathSeg = isLegacyStreetPathSegment(segAfterCity);
569            var isNamespacedStreetPath = isStreetSeoPrefixSegment(segAfterCity) && isStreetSeoSlugSegment(segAfterDistrict);
570            var isStreetPathSeg = isLegacyStreetPathSeg || isNamespacedStreetPath;
571            var districtVals = []
572                .concat(usp.getAll('district_id[]') || [])
573                .concat(usp.getAll('district_id') || []);
574            var districtUniq = toUniqueValues(districtVals);
575            var hasDistrictGroups = usp.has('district_group_id') || usp.has('district_group_id[]');
576            var hasStreetFilter = usp.has('street_id');
577            var singleDistrictCore = districtUniq.length === 1 && !hasDistrictGroups && !hasStreetFilter && !isStreetPathSeg;
578            var hasPreciseLocationFilter = districtUniq.length > 0 || hasDistrictGroups || hasStreetFilter;
579            var hasDistrictInPath = !isStreetPathSeg && (!!segAfterDistrict || (!!segAfterCity && hasPreciseLocationFilter));
580            if (hasDistrictInPath) {
581                usp.delete('district_id');
582                usp.delete('district_id[]');
583                usp.delete('district_group_id');
584                usp.delete('district_group_id[]');
585            }
586
587            if (isStreetPathSeg) {
588                try { usp.delete('street_id'); } catch (_eSt) {}
589            }
590
591            var etypeIdx = isNamespacedStreetPath ? -1 : (segAfterDistrict ? (cityIdx + 2) 
591: (cityIdx + 1));
592            if (etypeIdx >= 0 && pathParts[etypeIdx] && !isLegacyStreetPathSegment(pathParts[etypeIdx])) {
593                var etypeSlugInPath = String(pathParts[etypeIdx] || '').trim().toLowerCase();
594                var selectedPt = []
595                    .concat(usp.getAll('property_type[]') || [])
596                    .concat(usp.getAll('property_type') || []);
597                selectedPt = toUniqueValues(selectedPt);
598
599                var isExactGroupEncoded = false;
600                try {
601                    var cfg = o.topTypesConfig || ((typeof window !== 'undefined' && window.__mirelaTopTypesConfig) ? window.__mirelaTopTypesConfig : {});
602                    var groups = Array.isArray(cfg.groups) ? cfg.groups : [];
603                    var selectedSorted = selectedPt.slice().sort();
604                    groups.forEach(function (g) {
605                        if (!g) return;
606                        var gSlug = String(g.slug || '').trim().toLowerCase();
607                        if (!gSlug || gSlug !== etypeSlugInPath) return;
608                        var ids = (Array.isArray(g.type_ids) ? g.type_ids : [])
609                            .map(function (v) { return String(v || '').trim(); })
610                            .filter(Boolean)
611                            .sort();
612                        if (ids.length && ids.length === selectedSorted.length && ids.every(function (v, i) { return v === selectedSorted[i]; })) {
613                            isExactGroupEncoded = true;
614                        }
615                    });
616                } catch (_eCfg) {}
617
618                var isSingleTypeEncoded = selectedPt.length === 1;
619                if (isExactGroupEncoded || isSingleTypeEncoded) {
620                    usp.delete('property_type');
621                    usp.delete('property_type[]');
622                }
623            }
624        } catch (_e) {}
625
626        return usp;
627    }
628
629    /**
630     * When the list pathname already encodes location beyond city (district slug, street-*, etype, …),
631     * drop non-numeric ?s= — it duplicates the visible SEO path and hurts canonical URLs.
632     * Pure numeric ?s= is kept (offer-id search).
633     */
634    function dropRedundantLocationSParam(params, opts) {
635        var usp = (params instanceof URLSearchParams) ? params : new URLSearchParams();
636        var o = opts || {};
637        try {
638            var sVal = String(usp.get('s') || '').trim();
639            if (sVal === '' || /^\d+$/.test(sVal)) {
640                return usp;
641            }
642
643            var bp = String((o.basePath == null ? '' : o.basePath) || '').trim();
644            if (!bp) {
645                bp = String(typeof window !== 'undefined' ? (window.location.pathname || '') : '');
646            }
647
648            var pathParts = getPathSegments(bp);
649            var firstSeg = String(pathParts[0] || '').toLowerCase();
650            var hasLocale = isLocaleSegment(firstSeg, o);
651            var cityIdx = hasLocale ? 2 : 1;
652            var segAfterCity = String(pathParts[cityIdx + 1] || '').trim();
653
654            if (!segAfterCity) {
655                return usp;
656            }
657
658            usp.delete('s');
659        } catch (_e) {}
660
661        return usp;
662    }
663
664    function writeBrowseContractCanonicalFromServer(rawUrl) {
665        try {
666            if (typeof window === 'undefined') {
667                return;
668            }
669            var s = String(rawUrl || '').trim();
670            if (!s) {
671                return;
672            }
673            var u = new URL(s, window.location.origin);
674            var rel = (u.pathname || '') + (u.search || '') + (u.hash || '');
675            if (!rel || rel.charAt(0) !== '/') {
676                return;
677            }
678            window.__mirelaBrowseContractCanonicalRel = rel;
679            window.__mirelaBrowseContractCanonicalAt = Date.now();
680            window.__mirelaLastMapBrowseSeoHref = rel;
681        } catch (_eW) {}
682    }
683
684    /**
685     * @return string Relative path+query+hash (/prodazhbi?…); empty if none.
686     */
687    function readBrowseContractCanonicalRel() {
688        try {
689            var pri = String(
690                typeof window !== 'undefined' ? (window.__mirelaBrowseContractCanonicalRel || '') : ''
691            ).trim();
692            if (pri) {
693                var u1 = new URL(pri, window.location.origin);
694                return (u1.pathname || '') + (u1.search || '') + (u1.hash || '');
695            }
696        } catch (_e1) {}
697        try {
698            var fb = String(
699                typeof window !== 'undefined' ? (window.__mirelaLastMapBrowseSeoHref || '') : ''
700            ).trim();
701            if (fb) {
702                var u2 = new URL(fb, window.location.origin);
703                return (u2.pathname || '') + (u2.search || '') + (u2.hash || '');
704            }
705        } catch (_e2) {}
706        return '';
707    }
708
709    /** First path segment after optional locale — used to avoid cross–deal-type stale canonicals. */
710    function listingDealSlugKey(pathname) {
711        var parts = getPathSegments(pathname || '');
712        if (!parts.length) {
713            return '';
714        }
715        if (isLocaleSegment(parts[0])) {
716            parts.shift();
717        }
718
719        return String(parts[0] || '').toLowerCase();
720    }
721
722    api.writeBrowseContractCanonicalFromServer = writeBrowseContractCanonicalFromServer;
723    api.readBrowseContractCanonicalRel = readBrowseContractCanonicalRel;
724    api.listingDealSlugKey = listingDealSlugKey;
725
726    api.cleanupPathEncodedFilters = cleanupPathEncodedFilters;
727    api.dropRedundantLocationSParam = dropRedundantLocationSParam;
728})(window);
729
730// ---- END: js/components/search-canonical-utils.js ----
731
732// ---- START: js/components/search-form-query-utils.js ----
733/* Shared helpers for turning serialized search forms into query strings. */
734(function (window) {
735    'use strict';
736
737    var root = window.MirelaSearch = window.MirelaSearch || {};
738    var api = root.formQueryUtils = root.formQueryUtils || {};
739
740    function buildQueryString(formData, opts) {
741        var options = opts || {};
742        var remove = new Set(Array.isArray(options.remove) ? options.remove.map(String) : []);
743        var override = (options.override && typeof options.override === 'object') ? options.override : {};
744        var pairs = (Array.isArray(formData) ? formData : [])
745            .filter(function (item) { return item && item.name && item.name !== '_token'; })
746            .filter(function (item) { return item.value !== undefined && item.value !== null && String(item.value).trim() !== ''; })
747            .filter(function (item) { return !remove.has(String(item.name)); })
748            .filter(function (item) { return !Object.prototype.hasOwnProperty.call(override, String(item.name)); });
749
750        Object.keys(override).forEach(function (key) {
751            var value = override[key];
752            if (value === undefined || value === null || String(value).trim() === '') return;
753            pairs.push({ name: key, value: String(value) });
754        });
755
756        return pairs
757            .map(function (item) { return encodeURIComponent(item.name) + '=' + encodeURIComponent(item.value); })
758            .join('&');
759    }
760
761    function serializeJqueryForm($form, opts) {
762        try {
763            var pairs = serializeJqueryFormArray($form);
764            return buildQueryString(pairs, opts);
765        } catch (_e) {
766            return '';
767        }
768    }
769
770    /** Raw jQuery serializeArray (e.g. list offers pipeline); same filtering rules as consumers apply later. */
771    function serializeJqueryFormArray($form) {
772        try {
773            return ($form && $form.serializeArray) ? $form.serializeArray() : [];
774        } catch (_e) {
775            return [];
776        }
777    }
778
779    api.buildQueryString = buildQueryString;
780    api.serializeJqueryForm = serializeJqueryForm;
781    api.serializeJqueryFormArray = serializeJqueryFormArray;
782})(window);
783
784// ---- END: js/components/search-form-query-utils.js ----
785
786// ---- START: js/components/search-offer-type-filter-sync.js ----
787/**
788 * Shared: clearing price/area filter fields and syncing map bounds into .js-location-map
789 * when switching offer type (home tabs / list deal-type). UI shells differ; DOM targets align.
790 */
791(function (window, document) {
792    'use strict';
793
794    var root = (window.MirelaSearch = window.MirelaSearch || {});
795
796    function resolveForm(getSearchFormElement) {
797        if (typeof getSearchFormElement === 'function') {
798            try {
799                var f = getSearchFormElement();
800                if (f) return f;
801            } catch (_e) {}
802        }
803        try {
804            return document.querySelector('.js-search-form');
805        } catch (_e2) {
806            return null;
807        }
808    }
809
810    function patchSearchFilters(patch, reason) {
811        void reason;
812        try {
813            var target = (typeof window.filterState === 'object' && window.filterState) ? window.filterState : null;
814            if (!target) return false;
815            Object.keys(patch || {}).forEach(function (key) {
816                target[key] = String((patch || {})[key] == null ? '' : (patch || {})[key]).trim();
817            });
818            return true;
819        } catch (_eState) {
820            return false;
821        }
822    }
823
824    function clearSearchBarPriceAreaStorage() {
825        patchSearchFilters(
826            { price_from: '', price_to: '', area_from: '', area_to: '' },
827            'search-offer-type-filter-sync.clearSearchBarPriceAreaStorage'
828        );
829    }
830
831    function clearVisiblePriceInputs() {
832        try {
833            Array.prototype.slice.call(document.querySelectorAll(
834                '.wd-search-form input[name="price_from"], ' +
835                '.wd-search-form input[name="price_to"], ' +
836                '.wd-search-form input[name="price_from_eur"], ' +
837                '.wd-search-form input[name="price_to_eur"], ' +
838                '.wd-search-form .js-mirela-range-from, ' +
839                '.wd-search-form .js-mirela-range-to'
840            )).forEach(function (field) {
841                if (!field) return;
842                try {
843                    field.value = '';
844                } catch (_eValue) {}
845                try {
846                    field.dispatchEvent(new Event('input', { bubbles: true }));
847                } catch (_eInput) {}
848                try {
849                    field.dispatchEvent(new Event('change', { bubbles: true }));
850                } catch (_eChange) {}
851            });
852        } catch (_ePriceUi) {}
853    }
854
855    function clearTopRibbonPriceUi() {
856        try {
857            Array.prototype.slice.call(document.querySelectorAll('.js-top-price-dd')).forEach(function (dd) {
858                if (!dd) return;
859                try {
860                    dd.classList.remove('has-selection');
861                    dd.classList.remove('open');
862                } catch (_eDdCls) {}
863                try {
864                    var wrap = dd.closest('.top-mini-dd');
865                    if (wrap) wrap.classList.remove('open');
866                } catch (_eWrap) {}
867                try {
868                    if (dd.dataset) {
869                        dd.dataset.mirelaRangeOpenFrom = '';
870                        dd.dataset.mirelaRangeOpenTo = '';
871                    }
872                    dd.setAttribute('data-mirela-range-open-from', '');
873                    dd.setAttribute('data-mirela-range-open-to', '');
874                } catch (_eData) {}
875                try {
876                    var current = dd.querySelector('.current');
877                    var def = String((dd.getAttribute('data-default-label') || '').trim());
878                    if (current) current.textContent = def || 'Цена';
879                } catch (_eCurrent) {}
880                try {
881                    dd.querySelectorAll('.js-mirela-range-from, .js-mirela-range-to').forEach(function (field) {
882                        try { field.value = ''; } catch (_eFieldValue) {}
883                    });
884                } catch (_eFields) {}
885            });
886        } catch (_ePriceRibbon) {}
887    }
888
889    function normalizeDealType(typeValue) {
890        var normalized = String(typeValue == null ? '' : typeValue).trim();
891        return normalized === '1' || normalized === '2' || normalized === '3' || normalized === '4' ? normalized : '';
892    }
893
894    function shouldClearPriceOnDealTypeSwitch(previousType, nextType) {
895        var prev = normalizeDealType(previousType);
896        var next = normalizeDealType(nextType);
897        if (!prev || !next || prev === next) return false;
898        if ((prev === '1' && next === '3') || (prev === '2' && next === '4')) return false;
899        if ((prev === '4' && next === '2') || (prev === '3' && next === '1')) return false;
900        return true;
901    }
902
903    /** List + any page: centralized ribbon (+ filterState price), SearchBar slice, optional URL strip. */
904    function clearPriceFieldsAndUrl(deps) {
905        var d = deps || {};
906        if (d.skipWhenDealTypePairDisallows === true && !shouldClearPriceOnDealTypeSwitch(d.previousType, d.nextType)) {
907            return false;
908        }
909        var form = resolveForm(d.getSearchFormElement);
910        try {
911            if (form) {
912                var hFrom = form.querySelector('input[name="price_from"]');
913                var hTo = form.querySelector('input[name="price_to"]');
914                if (hFrom) hFrom.value = '';
915                if (hTo) hTo.value = '';
916            }
917            try {
918                if (typeof window.clearRibbonPriceFilterState === 'function') {
919                    window.clearRibbonPriceFilterState();
920                }
921            } catch (_eFs) {}
922            patchSearchFilters({ price_from: '', price_to: '' }, 'search-offer-type-filter-sync.clearPriceFieldsAndUrl');
923            clearVisiblePriceInputs();
924            clearTopRibbonPriceUi();
925            try {
926                if (window.MirelaRangeFilters && typeof window.MirelaRangeFilters.refreshAll === 'function') {
927                    window.MirelaRangeFilters.refreshAll();
928                }
929            } catch (_ref) {}
930            if (typeof d.getCurrentSearchParams === 'function' && typeof d.replaceCurrentUrlFromParams === 'function') {
931                try {
932                    var usp = d.getCurrentSearchParams();
933                    var changed = false;
934                    if (usp && usp.has && usp.has('price_from')) {
935                        usp.delete('price_from');
936                        changed = true;
937                    }
938                    if (usp && usp.has && usp.has('price_to')) {
939                        usp.delete('price_to');
940                        changed = true;
941                    }
942                    if (changed) {
943                        d.replaceCurrentUrlFromParams(usp, true);
944                    }
945                } catch (_eUrl) {}
946            }
947        } catch (_e) {}
948        return true;
949    }
950
951    /** Home tabs 1↔2: price + area fields + quick dropdown labels + SearchBar stored area. */
952    function clearPriceAreaAndQuickLabelsForHome() {
953        try {
954            if (window.MirelaRangeFilters && typeof window.MirelaRangeFilters.clearAllRanges === 'function') {
955                window.MirelaRangeFilters.clearAllRanges();
956                clearSearchBarPriceAreaStorage();
957                return;
958            }
959        } catch (_eMr) {}
960        try {
961            var $ = window.jQuery;
962            if ($ && $.fn) {
963                $('.js-mirela-range-from').val('');
964                $('.js-mirela-range-to').val('');
965                var priceDd = $('.js-top-price-dd');
966                if (priceDd.length) {
967                    var defaultLabel =
968                        priceDd.data('defaultLabel') ||
969                        (document.documentElement.lang === 'bg' ? 'Цена' : 'Price');
970                    priceDd.find('.current').text(defaultLabel);
971                    priceDd.removeClass('has-selection');
972                }
973                var areaDd = $('.js-top-area-dd');
974                if (areaDd.length) {
975                    var defaultArea =
976                        areaDd.data('defaultLabel') ||
977                        (document.documentElement.lang === 'bg' ? 'Площ' : 'Area');
978                    areaDd.find('.current').text(defaultArea);
979                    areaDd.removeClass('has-selection');
980                }
981            }
982        } catch (_eJq) {}
983        clearSearchBarPriceAreaStorage();
984    }
985
986    /**
987     * If map hidden input is empty, write current Leaflet bounds pipe string (ne|sw|zoom).
988     * @param {HTMLInputElement|null|undefined} mapInputEl
989     * @param {*} MapLeaflet — module with getMap()
990     */
991    function fillMapBoundsFromLeafletIfEmpty(mapInputEl, MapLeaflet) {
992        try {
993            if (!mapInputEl || !MapLeaflet || typeof MapLeaflet.getMap !== 'function') return;
994            var v = String(mapInputEl.value || '').trim();
995            if (v) return;
996            var m = MapLeaflet.getMap();
997            if (!m || typeof m.getBounds !== 'function' || typeof m.getZoom !== 'function') return;
998            var b = m.getBounds();
999            if (!b || typeof b.getNorthEast !== 'function' || typeof b.getSouthWest !== 'function') return;
1000            var ne = b.getNorthEast();
1001            var sw = b.getSouthWest();
1002            if (!ne || !sw) return;
1003            mapInputEl.value = [
1004                ne.lng.toFixed(5),
1005                ne.lat.toFixed(5),
1006                sw.lng.toFixed(5),
1007                sw.lat.toFixed(5),
1008                m.getZoom()
1009            ].join('|');
1010        } catch (_e) {}
1011    }
1012
1013    root.searchOfferTypeFilterSync = {
1014        shouldClearPriceOnDealTypeSwitch: shouldClearPriceOnDealTypeSwitch,
1015        clearPriceFieldsAndUrl: clearPriceFieldsAndUrl,
1016        clearPriceAreaAndQuickLabelsForHome: clearPriceAreaAndQuickLabelsForHome,
1017        fillMapBoundsFromLeafletIfEmpty: fillMapBoundsFromLeafletIfEmpty
1018    };
1019})(window, document);
1020
1021// ---- END: js/components/search-offer-type-filter-sync.js ----
1022
1023// ---- START: js/components/location/location-selection-request.js ----
1024/* Pure helpers for location selection request payloads. */
1025(function (window) {
1026    'use strict';
1027
1028    var api = window.MirelaLocationSelectionRequest = window.MirelaLocationSelectionRequest || {};
1029
1030    function getNormalizedCityId(value) {
1031        try {
1032            if (!value) return '';
1033            var cities = value.cities;
1034            return Array.isArray(cities) ? String(cities[0] || '').trim() : String(cities || '').trim();
1035        } catch (_e) {
1036            return '';
1037        }
1038    }
1039
1040    function toCsv(value) {
1041        try {
1042            if (Array.isArray(value)) {
1043                return value.filter(Boolean).map(function (v) { return String(v); }).join(',');
1044            }
1045            return value == null ? '' : String(value);
1046        } catch (_e) {
1047            return '';
1048        }
1049    }
1050
1051    function hasMapBoundsValue(mapValue) {
1052        try {
1053            var mapStr = String(mapValue || '').trim();
1054            return !!(mapStr && mapStr.split('|').length >= 4);
1055        } catch (_e) {
1056            return false;
1057        }
1058    }
1059
1060    /**
1061     * Build an AJAX-ready payload from a normalized or legacy location selection object.
1062     */
1063    function buildSelectionRequestData(value, opts) {
1064        var options = opts || {};
1065        var normalizedCityId = getNormalizedCityId(value);
1066        var mode = 'empty';
1067        var ajaxData = {};
1068
1069        try {
1070            if (value && value.districts) {
1071                mode = 'districts';
1072                ajaxData.district_id = value.districts;
1073                if (normalizedCityId && options.includeCity !== false) ajaxData.city_id = normalizedCityId;
1074            } else if (value && value.district_groups) {
1075                mode = 'district_groups';
1076                ajaxData.district_group_id = options.forceGroupArray && !Array.isArray(value.district_groups)
1077                    ? [value.district_groups]
1078                    : value.district_groups;
1079                if (normalizedCityId && options.includeCity !== false) ajaxData.city_id = normalizedCityId;
1080            } else if (value && value.street_id) {
1081                mode = 'street';
1082                ajaxData.street_id = value.street_id;
1083                if (normalizedCityId && options.includeCity !== false) ajaxData.city_id = normalizedCityId;
1084            } else if (normalizedCityId) {
1085                mode = 'city';
1086                if (options.includeCity !== false) ajaxData.city_id = normalizedCityId;
1087                if (options.forceCityOnce) {
1088                    try { window.__forceCityIdOnce = String(normalizedCityId || ''); } catch (_eForce) {}
1089                }
1090            }
1091        } catch (_e) {}
1092
1093        return {
1094            cityId: normalizedCityId,
1095            mode: mode,
1096            ajaxData: ajaxData
1097        };
1098    }
1099
1100    api.getNormalizedCityId = getNormalizedCityId;
1101    api.toCsv = toCsv;
1102    api.hasMapBoundsValue = hasMapBoundsValue;
1103    api.buildSelectionRequestData = buildSelectionRequestData;
1104})(window);
1105
1106// ---- END: js/components/location/location-selection-request.js ----
1107
1108// ---- START: js/components/location/home-location-navigation-policy.js ----
1109/* FilterState-first home location vs map navigation policy. */
1110(function (window) {
1111    'use strict';
1112
1113    var api = window.MirelaHomeLocationNavigationPolicy = window.MirelaHomeLocationNavigationPolicy || {};
1114
1115    function readFilterStateSnapshot() {
1116        try {
1117            return (typeof window.filterState === 'object' && window.filterState) ? window.filterState : null;
1118        } catch (_e) {
1119            return null;
1120        }
1121    }
1122
1123    function readStringField(state, key) {
1124        try {
1125            if (!state || !Object.prototype.hasOwnProperty.call(state, key)) return '';
1126            return String(state[key] || '').trim();
1127        } catch (_e) {
1128            return '';
1129        }
1130    }
1131
1132    function readArrayField(state, key) {
1133        try {
1134            var value = state && Object.prototype.hasOwnProperty.call(state, key) ? state[key] : null;
1135            if (!Array.isArray(value)) return [];
1136            return value.map(function (item) { return String(item || '').trim(); }
1136).filter(Boolean);
1137        } catch (_e) {
1138            return [];
1139        }
1140    }
1141
1142    function hasStrictLocationState(state, overrides) {
1143        var source = state || readFilterStateSnapshot() || {};
1144        var options = overrides || {};
1145        try {
1146            var streetId = Object.prototype.hasOwnProperty.call(options, 'streetId')
1147                ? String(options.streetId || '').trim()
1148                : readStringField(source, 'street_id');
1149            if (streetId) return true;
1150
1151            var districtIds = Object.prototype.hasOwnProperty.call(options, 'districtIds')
1152                ? (Array.isArray(options.districtIds) ? options.districtIds : [])
1153                : readArrayField(source, 'district_id');
1154            var groupIds = Object.prototype.hasOwnProperty.call(options, 'groupIds')
1155                ? (Array.isArray(options.groupIds) ? options.groupIds : [])
1156                : readArrayField(source, 'district_group_id');
1157
1158            return districtIds.length > 0 || groupIds.length > 0;
1159        } catch (_e) {
1160            return false;
1161        }
1162    }
1163
1164    function shouldDropCityForBounds(opts) {
1165        var options = opts || {};
1166        try {
1167            var hasMapBounds = !!options.hasMapBounds;
1168            var hasStrictLocation = !!options.hasStrictLocation;
1169            var searchTerm = String(options.searchTerm || '').trim();
1170            var cityId = String(options.cityId || '').trim();
1171            var defaultCityId = String(options.defaultCityId || '').trim();
1172            if (!hasMapBounds || hasStrictLocation || searchTerm !== '') return false;
1173            return (!cityId || cityId === '0' || (defaultCityId && cityId === defaultCityId));
1174        } catch (_e) {
1175            return false;
1176        }
1177    }
1178
1179    function clearHomeMapIntentFlags() {
1180        try { window.__mirelaHomeForceMapListNext = false; } catch (_e0) {}
1181        try { window.__mirelaHomeManualBoundsSearchDone = false; } catch (_e1) {}
1182        try { window.__mapManualNext = false; } catch (_e2) {}
1183        try { window.__manualSearchRequested = false; } catch (_e3) {}
1184    }
1185
1186    function setHomeListIntent(mode) {
1187        var normalized = '';
1188        try {
1189            normalized = String(mode || '').trim().toLowerCase();
1190        } catch (_eNorm) {
1191            normalized = '';
1192        }
1193        if (normalized !== 'map' && normalized !== 'location') normalized = '';
1194
1195        try { window.__mirelaHomeListIntent = normalized; } catch (_eWrite) {}
1196
1197        try {
1198            if (window.isHomePage !== true) return normalized;
1199            if (normalized === 'location') {
1200                clearHomeMapIntentFlags();
1201                try {
1202                    if (window.MirelaSearch && typeof window.MirelaSearch.applyUserMovedMapHome === 'function') {
1203                        window.MirelaSearch.applyUserMovedMapHome(false);
1204                    } else {
1205                        window.__mirelaUserMovedMapHome = false;
1206                    }
1207                } catch (_eMoved) {}
1208            } else if (normalized === 'map') {
1209                try { window.__mirelaHomeCityScopedBrowseNext = false; } catch (_eCityScope) {}
1210            }
1211        } catch (_e) {}
1212
1213        return normalized;
1214    }
1215
1216    function getHomeDefaultCityId(opts) {
1217        var options = opts || {};
1218        var defaultCityId = '';
1219        try {
1220            defaultCityId = String(options.defaultCityId || '').trim();
1221        } catch (_eOpt) {
1222            defaultCityId = '';
1223        }
1224        if (defaultCityId) return defaultCityId;
1225        try {
1226            if (window.__mirelaHomeDefaultListCityId != null) {
1227                defaultCityId = String(window.__mirelaHomeDefaultListCityId || '').trim();
1228            }
1229        } catch (_eGlobal) {
1230            defaultCityId = '';
1231        }
1232        return defaultCityId;
1233    }
1234
1235    function paramObjLocksHomeListToMapViewport(paramObj) {
1236        var allowKeys = {
1237            type: true,
1238            deal: true,
1239            map: true,
1240            map_manual: true,
1241            map_bounds: true,
1242            map_user: true,
1243            map_lat: true,
1244            map_lng: true,
1245            map_zoom: true,
1246            page: true,
1247            city_id: true,
1248            'city_id[]': true,
1249            s: true,
1250            home_preload: true,
1251            _: true,
1252            _token: true,
1253            locale: true,
1254            focus_offer_id: true,
1255            restore_scroll: true
1256        };
1257        try {
1258            if (!paramObj || typeof paramObj !== 'object') return false;
1259            var keys = Object.keys(paramObj);
1260            for (var i = 0; i < keys.length; i++) {
1261                var key = keys[i];
1262                if (allowKeys[key]) continue;
1263                var value = paramObj[key];
1264                if (value === null || value === undefined || value === '') continue;
1265                if (value === '0' || value === 0 || value === false) continue;
1266                if (Array.isArray(value) && value.length === 0) continue;
1267                return true;
1268            }
1269        } catch (_e) {}
1270        return false;
1271    }
1272
1273    /**
1274     * Decide whether home list navigation should use location navigation or map/bounds navigation.
1275     * Reads `filterState` first and accepts field overrides from callers.
1276     */
1277    function getHomeListNavigationState(opts) {
1278        var options = opts || {};
1279        var state = options.filterState || readFilterStateSnapshot() || {};
1280
1281        var mapValue = Object.prototype.hasOwnProperty.call(options, 'mapValue')
1282            ? String(options.mapValue || '').trim()
1283            : readStringField(state, 'map');
1284        var cityId = Object.prototype.hasOwnProperty.call(options, 'cityId')
1285            ? String(options.cityId || '').trim()
1286            : readStringField(state, 'city_id');
1287        var searchTerm = Object.prototype.hasOwnProperty.call(options, 'searchTerm')
1288            ? String(options.searchTerm || '').trim()
1289            : readStringField(state, 's');
1290        var intent = '';
1291
1292        try {
1293            intent = String(window.__mirelaHomeListIntent || '').trim().toLowerCase();
1294        } catch (_eIntent) {
1295            intent = '';
1296        }
1297
1298        var hasStrictLocation = Object.prototype.hasOwnProperty.call(options, 'hasStrictLocation')
1299            ? !!options.hasStrictLocation
1300            : hasStrictLocationState(state, options);
1301        var hasMapBounds = Object.prototype.hasOwnProperty.call(options, 'hasMapBounds')
1302            ? !!options.hasMapBounds
1303            : api.hasMapBoundsValue(mapValue);
1304        var defaultCityId = getHomeDefaultCityId(options);
1305        var hasExplicitCity = !!(
1306            cityId &&
1307            cityId !== '0' &&
1308            (intent === 'location' || searchTerm !== '' || !defaultCityId || cityId !== defaultCityId)
1309        );
1310        var hasExplicitLocation = !!(hasStrictLocation || hasExplicitCity);
1311        var hasFallbackMapIntent = false;
1312        var locksListToViewport = false;
1313
1314        try {
1315            hasFallbackMapIntent = !!(
1316                options.forceMapIntent === true ||
1317                intent === 'map' ||
1318                window.__mirelaHomeForceMapListNext === true ||
1319                window.__mirelaUserMovedMapHome === true ||
1320                window.__mirelaHomeManualBoundsSearchDone === true
1321            );
1322        } catch (_eFallback) {
1323            hasFallbackMapIntent = (options.forceMapIntent === true || intent === 'map');
1324        }
1325
1326        try {
1327            locksListToViewport = !!(
1328                hasMapBounds &&
1329                !hasStrictLocation &&
1330                options.paramObj &&
1331                paramObjLocksHomeListToMapViewport(options.paramObj)
1332            );
1333        } catch (_eLockViewport) {
1334            locksListToViewport = false;
1335        }
1336
1337        if (locksListToViewport) {
1338            hasExplicitCity = false;
1339            hasExplicitLocation = false;
1340            hasFallbackMapIntent = true;
1341        }
1342
1343        var shouldUseLocationNavigation = !!(hasExplicitLocation && intent !== 'map');
1344        var shouldUseMapNavigation = !!(hasMapBounds && !hasExplicitLocation && hasFallbackMapIntent);
1345
1346        if (intent === 'location' && hasExplicitLocation) {
1347            shouldUseLocationNavigation = true;
1348            shouldUseMapNavigation = false;
1349        }
1350        if (options.forceMapIntent === true && hasMapBounds && !hasExplicitLocation) {
1351            shouldUseMapNavigation = true;
1352            shouldUseLocationNavigation = false;
1353        }
1354        if (hasStrictLocation) {
1355            shouldUseLocationNavigation = true;
1356            shouldUseMapNavigation = false;
1357        }
1358
1359        return {
1360            intent: intent,
1361            cityId: cityId,
1362            searchTerm: searchTerm,
1363            defaultCityId: defaultCityId,
1364            hasMapBounds: hasMapBounds,
1365            hasStrictLocation: hasStrictLocation,
1366            hasExplicitCity: hasExplicitCity,
1367            hasExplicitLocation: hasExplicitLocation,
1368            locksListToViewport: locksListToViewport,
1369            shouldUseLocationNavigation: shouldUseLocationNavigation,
1370            shouldUseMapNavigation: shouldUseMapNavigation
1371        };
1372    }
1373
1374    api.readFilterStateSnapshot = readFilterStateSnapshot;
1375    api.getHomeDefaultCityId = getHomeDefaultCityId;
1376    api.hasStrictLocationState = hasStrictLocationState;
1377    api.hasMapBoundsValue = function (mapValue) {
1378        return !!(window.MirelaLocationSelectionRequest && typeof window.MirelaLocationSelectionRequest.hasMapBoundsValue === 'function'
1379            ? window.MirelaLocationSelectionRequest.hasMapBoundsValue(mapValue)
1380            : false);
1381    };
1382    api.paramObjLocksHomeListToMapViewport = paramObjLocksHomeListToMapViewport;
1383    api.shouldDropCityForBounds = shouldDropCityForBounds;
1384    api.clearHomeMapIntentFlags = clearHomeMapIntentFlags;
1385    api.setHomeListIntent = setHomeListIntent;
1386    api.getHomeListNavigationState = getHomeListNavigationState;
1387})(window);
1388
1389// ---- END: js/components/location/home-location-navigation-policy.js ----
1390
1391// ---- START: js/components/location/location-form-legacy-compat.js ----
1392/* Legacy form compatibility helpers for location selection. */
1393(function (window) {
1394    'use strict';
1395
1396    var api = window.MirelaLocationFormLegacyCompat = window.MirelaLocationFormLegacyCompat || {};
1397    var root = window.MirelaSearch = window.MirelaSearch || {};
1398
1399    function getLocationController(reason) {
1400        try {
1401            var uiControllers = root.searchUiControllers || null;
1402            return uiControllers && typeof uiControllers.createLocationFilterController === 'function'
1403                ? uiControllers.createLocationFilterController({ reason: reason || 'locationFormLegacyCompat' })
1404                : null;
1405        } catch (_e) {
1406            return null;
1407        }
1408    }
1409
1410    function getRequestApi() {
1411        return window.MirelaLocationSelectionRequest || null;
1412    }
1413
1414    function getHomePolicyApi() {
1415        return window.MirelaHomeLocationNavigationPolicy || null;
1416    }
1417
1418    function getNormalizedCityId(value) {
1419        var requestApi = getRequestApi();
1420        if (requestApi && typeof requestApi.getNormalizedCityId === 'function') {
1421            return requestApi.getNormalizedCityId(value);
1422        }
1423        return '';
1424    }
1425
1426    function toCsv(value) {
1427        var requestApi = getRequestApi();
1428        if (requestApi && typeof requestApi.toCsv === 'function') {
1429            return requestApi.toCsv(value);
1430        }
1431        return '';
1432    }
1433
1434    function ensureHiddenInput($form, name) {
1435        try {
1436            if (!$form || !$form.length || !window.jQuery) return null;
1437            var $element = $form.find('[name="' + name + '"]');
1438            if (!$element.length) {
1439                $element = window.jQuery('<input type="hidden"/>').attr('name', name).appendTo($form);
1440            }
1441            return $element;
1442        } catch (_e) {
1443            return null;
1444        }
1445    }
1446
1447    function ensureHiddenInputElement(form, name) {
1448        try {
1449            if (!form || !form.querySelectorAll) return null;
1450            var nodes = Array.prototype.slice.call(form.querySelectorAll('input[name="' + name + '"]'));
1451            var field = nodes.length ? nodes[0] : null;
1452            nodes.slice(1).forEach(function (node) {
1453                try { node.parentNode && node.parentNode.removeChild(node); } catch (_eNode) {}
1454            });
1455            if (!field) {
1456                field = document.createElement('input');
1457                field.type = 'hidden';
1458                field.name = name;
1459                form.appendChild(field);
1460            }
1461            return field;
1462        } catch (_e) {
1463            return null;
1464        }
1465    }
1466
1467    function setSingleHiddenValue(form, name, value) {
1468        try {
1469            var field = ensureHiddenInputElement(form, name);
1470            if (field) {
1471                field.value = String(value == null ? '' : value);
1472            }
1473            return field;
1474        } catch (_e) {
1475            return null;
1476        }
1477    }
1478
1479    function removeLocationArrayInputs(form, opts) {
1480        var options = opts || {};
1481        try {
1482            if (!form || !form.querySelectorAll) return;
1483            var names = [];
1484            if (options.includeDistrict !== false) names.push('district_id[]');
1485            if (options.includeDistrictGroup !== false) names.push('district_group_id[]');
1486            if (options.includeCity === true) names.push('city_id[]');
1487            if (options.includeStreet === true) names.push('street_id[]');
1488
1489            names.forEach(function (name) {
1490                Array.prototype.slice.call(form.querySelectorAll('input[name="' + name + '"]')).forEach(function (node) {
1491                    try { node.parentNode && node.parentNode.removeChild(node); } catch (_eNode) {}
1492                });
1493            });
1494
1495            try {
1496                Array.from(form.querySelectorAll('input')).forEach(function (node) {
1497                    var inputName = String((node && node.name) || '');
1498                    if (/^(city_id|district_id|district_group_id|street_id)\[\d+\]$/.test(inputName)) {
1499                        if ((options.includeCity === true && inputName.indexOf('city_id[') === 0) ||
1500                            ((options.includeDistrict !== false || options.includeDistrictGroup !== false || options.includeStreet === true) && inputName !== '')) {
1501                            try { node.parentNode && node.parentNode.removeChild(node); } catch (_eIndexed) {}
1502                        }
1503                    }
1504                });
1505            } catch (_eIdx) {}
1506        } catch (_e) {}
1507    }
1508
1509    function setFieldValue($form, selectors, name, value, ensureHidden) {
1510        try {
1511            var safeValue = (value == null) ? '' : String(value);
1512            if (ensureHidden) {
1513                var $hidden = ensureHiddenInput($form, name);
1514                if ($hidden && $hidden.length) {
1515                    $hidden.val(safeValue);
1516                }
1517            }
1518            if ($form && $form.length && selectors) {
1519                $form.find(selectors).val(safeValue);
1520            }
1521        } catch (_e) {}
1522    }
1523
1524    function removeHiddenInputsByNames(form, names) {
1525        try {
1526            if (!form || !form.querySelectorAll) return;
1527            (names || []).forEach(function (name) {
1528                var key = String(name || '').trim();
1529                if (!key) return;
1530                Array.prototype.slice.call(form.querySelectorAll('input[name="' + key + '"]')).forEach(function (node) {
1531                    try { node.parentNode && node.parentNode.removeChild(node); } catch (_eNode) {}
1532                });
1533            });
1534        } catch (_e) {}
1535    }
1536
1537    function clearCommittedLocationContext($form, opts) {
1538        var options = opts || {};
1539        try {
1540            if (!$form || !$form.length) return;
1541            var formNode = $form.get(0);
1542            if (formNode) {
1543                removeLocationArrayInputs(formNode, {
1544                    includeDistrict: true,
1545                    includeDistrictGroup: true,
1546                    includeCity: true,
1547                    includeStreet: true
1548                });
1549                removeHiddenInputsByNames(formNode, options.removeMapManual === false ? [] : ['map_manual']);
1550            }
1551
1552            if (options.includeSearch !== false) {
1553                setFieldValue($form, '.js-location-dd-input, .js-search-input, [name="s"]', 's', '', true);
1554            }
1555            setFieldValue($form, options.citySelectors || '.js-city-id, .js-mobile-city-id, [name="city_id"]', 'city_id', '', true);
1556            setFieldValue($form, options.districtSelectors || '.js-district-id, [name="district_id"]', 'district_id', '', true);
1557            setFieldValue($form, options.groupSelectors || '.js-district-group-id, [name="district_group_id"]', 'district_group_id', '', true);
1558            setFieldValue($form, options.streetSelectors || '.js-street
1558-id, [name="street_id"]', 'street_id', '', true);
1559
1560            if (options.clearMap !== false) {
1561                setFieldValue($form, options.mapSelectors || '.js-location-map, [name="map"]', 'map', '', true);
1562            }
1563
1564            try {
1565                var locationController = getLocationController('locationFormLegacyCompat.clearCommittedLocationContext');
1566                if (locationController && typeof locationController.applySelection === 'function') {
1567                    locationController.applySelection({
1568                        searchLabel: '',
1569                        cityId: '',
1570                        districtId: '',
1571                        groupId: '',
1572                        streetId: '',
1573                        districtIds: [],
1574                        groupIds: [],
1575                        value: null,
1576                        clearMap: options.clearMap !== false
1577                    });
1578                }
1579            } catch (_eState) {}
1580        } catch (_e) {}
1581    }
1582
1583    function applySelectionToJqueryForm($form, value, opts) {
1584        var options = opts || {};
1585        var normalizedCityId = getNormalizedCityId(value);
1586        var mode = 'empty';
1587
1588        try {
1589            if (!$form || !$form.length) {
1590                return { cityId: normalizedCityId, mode: mode };
1591            }
1592
1593            try {
1594                var hasLocationPick = !!(
1595                    value &&
1596                    (normalizedCityId || value.districts || value.district_groups || value.street_id)
1597                );
1598                if (hasLocationPick && window.isHomePage === true) {
1599                    try {
1600                        if (window.MirelaSearch && typeof window.MirelaSearch.applyUserMovedMapHome === 'function') {
1601                            window.MirelaSearch.applyUserMovedMapHome(false);
1602                        } else {
1603                            window.__mirelaUserMovedMapHome = false;
1604                        }
1605                    } catch (_eMovedHome) {}
1606
1607                    var homePolicyApi = getHomePolicyApi();
1608                    if (homePolicyApi && typeof homePolicyApi.setHomeListIntent === 'function') {
1609                        homePolicyApi.setHomeListIntent('location');
1610                    }
1611                }
1612            } catch (_eHomeGesture) {}
1613
1614            clearCommittedLocationContext($form, {
1615                includeSearch: options.searchLabel !== undefined,
1616                clearMap: options.clearMap !== false,
1617                citySelectors: options.citySelectors,
1618                districtSelectors: options.districtSelectors,
1619                groupSelectors: options.groupSelectors,
1620                streetSelectors: options.streetSelectors,
1621                mapSelectors: options.mapSelectors
1622            });
1623
1624            if (options.searchLabel !== undefined) {
1625                setFieldValue($form, '', 's', String(options.searchLabel || ''), true);
1626            }
1627
1628            setFieldValue($form, options.citySelectors || '.js-city-id, [name="city_id"]', 'city_id', normalizedCityId, !!options.ensureCityHidden);
1629
1630            if (value && value.districts) {
1631                mode = 'districts';
1632                setFieldValue($form, options.districtSelectors || '.js-district-id, [name="district_id"]', 'district_id', toCsv(value.districts), !!options.ensureLocationHidden);
1633                setFieldValue($form, options.groupSelectors || '.js-district-group-id, [name="district_group_id"]', 'district_group_id', '', !!options.ensureLocationHidden);
1634                setFieldValue($form, options.streetSelectors || '.js-street-id, [name="street_id"]', 'street_id', '', !!options.ensureLocationHidden);
1635            } else if (value && value.district_groups) {
1636                mode = 'district_groups';
1637                setFieldValue($form, options.groupSelectors || '.js-district-group-id, [name="district_group_id"]', 'district_group_id', toCsv(value.district_groups), !!options.ensureLocationHidden);
1638                setFieldValue($form, options.districtSelectors || '.js-district-id, [name="district_id"]', 'district_id', '', !!options.ensureLocationHidden);
1639                setFieldValue($form, options.streetSelectors || '.js-street
1639-id, [name="street_id"]', 'street_id', '', !!options.ensureLocationHidden);
1640            } else if (value && value.street_id) {
1641                mode = 'street';
1642                setFieldValue($form, options.districtSelectors || '.js-district-id, [name="district_id"]', 'district_id', '', !!options.ensureLocationHidden);
1643                setFieldValue($form, options.groupSelectors || '.js-district-group-id, [name="district_group_id"]', 'district_group_id', '', !!options.ensureLocationHidden);
1644                setFieldValue($form, options.streetSelectors || '.js-street-id, [name="street_id"]', 'street_id', String(value.street_id || ''), !!options.ensureLocationHidden);
1645            } else if (normalizedCityId) {
1646                mode = 'city';
1647                setFieldValue($form, options.districtSelectors || '.js-district-id, [name="district_id"]', 'district_id', '', !!options.ensureLocationHidden);
1648                setFieldValue($form, options.groupSelectors || '.js-district-group-id, [name="district_group_id"]', 'district_group_id', '', !!options.ensureLocationHidden);
1649                setFieldValue($form, options.streetSelectors || '.js-street-id, [name="street_id"]', 'street_id', '', !!options.ensureLocationHidden);
1650            }
1651
1652            if (options.clearMap) {
1653                try { $form.find(options.mapSelectors || '.js-location-map, [name="map"]').val(''); } catch (_eClearMap) {}
1654            }
1655            if (options.persistCityPreset) {
1656                try { window.__mirelaPresetCityIdTransient = String(normalizedCityId || ''); } catch (_ePreset) {}
1657            }
1658            if (options.forceCityOnce) {
1659                try { window.__forceCityIdOnce = String(normalizedCityId || ''); } catch (_eForce) {}
1660            }
1661
1662            try {
1663                var locationController = getLocationController('locationFormLegacyCompat.applySelectionToJqueryForm');
1664                if (locationController && typeof locationController.applySelection === 'function') {
1665                    locationController.applySelection({
1666                        searchLabel: options.searchLabel !== undefined ? String(options.searchLabel || '') : '',
1667                        cityId: normalizedCityId,
1668                        districtId: value && value.districts ? toCsv(value.districts) : '',
1669                        groupId: value && value.district_groups ? toCsv(value.district_groups) : '',
1670                        streetId: value && value.street_id ? String(value.street_id || '') : '',
1671                        districtIds: value && value.districts ? [].concat(value.districts).map(String) : [],
1672                        groupIds: value && value.district_groups ? [].concat(value.district_groups).map(String) : [],
1673                        value: value || null,
1674                        clearMap: options.clearMap === true
1675                    });
1676                }
1677            } catch (_eStatePatch) {}
1678        } catch (_e) {}
1679
1680        return {
1681            cityId: normalizedCityId,
1682            mode: mode
1683        };
1684    }
1685
1686    function hasLocationFiltersInParams(params, opts) {
1687        var usp = params instanceof URLSearchParams ? params : new URLSearchParams(String(params || ''));
1688        var options = opts || {};
1689        try {
1690            var hasStrict = !!(
1691                String(usp.get('district_id') || '').trim() !== '' ||
1692                String(usp.get('district_group_id') || '').trim() !== '' ||
1693                String(usp.get('street_id') || '').trim() !== '' ||
1694                (usp.getAll('district_id[]') || []).length > 0 ||
1695                (usp.getAll('district_group_id[]') || []).length > 0
1696            );
1697            if (hasStrict) return true;
1698            if (options.includeCity) {
1699                if (String(usp.get('city_id') || '').trim() !== '' || (usp.getAll('city_id[]') || []).length > 0) {
1700                    return true;
1701                }
1702            }
1703            if (options.includeMap) {
1704                if (String(usp.get('map') || '').trim() !== '') {
1705                    return true;
1706                }
1707            }
1708        } catch (_e) {}
1709        return false;
1710    }
1711
1712    api.ensureHiddenInput = ensureHiddenInput;
1713    api.ensureHiddenInputElement = ensureHiddenInputElement;
1714    api.setSingleHiddenValue = setSingleHiddenValue;
1715    api.removeLocationArrayInputs = removeLocationArrayInputs;
1716    api.clearCommittedLocationContext = clearCommittedLocationContext;
1717    api.applySelectionToJqueryForm = applySelectionToJqueryForm;
1718    api.hasLocationFiltersInParams = hasLocationFiltersInParams;
1719})(window);
1720
1721// ---- END: js/components/location/location-form-legacy-compat.js ----
1722
1723// ---- START: js/components/search-location-form-utils.js ----
1724/* Thin compatibility facade around split location modules. */
1725(function (window) {
1726    'use strict';
1727
1728    var root = window.MirelaSearch = window.MirelaSearch || {};
1729    var api = root.locationFormUtils = root.locationFormUtils || {};
1730
1731    function getRequestApi() {
1732        return window.MirelaLocationSelectionRequest || null;
1733    }
1734
1735    function getHomePolicyApi() {
1736        return window.MirelaHomeLocationNavigationPolicy || null;
1737    }
1738
1739    function getLegacyCompatApi() {
1740        return window.MirelaLocationFormLegacyCompat || null;
1741    }
1742
1743    function bindMethod(methodName, fallbackValue) {
1744        return function () {
1745            var requestApi = getRequestApi();
1746            if (requestApi && typeof requestApi[methodName] === 'function') {
1747                return requestApi[methodName].apply(requestApi, arguments);
1748            }
1749            var homePolicyApi = getHomePolicyApi();
1750            if (homePolicyApi && typeof homePolicyApi[methodName] === 'function') {
1751                return homePolicyApi[methodName].apply(homePolicyApi, arguments);
1752            }
1753            var legacyCompatApi = getLegacyCompatApi();
1754            if (legacyCompatApi && typeof legacyCompatApi[methodName] === 'function') {
1755                return legacyCompatApi[methodName].apply(legacyCompatApi, arguments);
1756            }
1757            return fallbackValue;
1758        };
1759    }
1760
1761    api.getNormalizedCityId = bindMethod('getNormalizedCityId', '');
1762    api.toCsv = bindMethod('toCsv', '');
1763    api.ensureHiddenInput = bindMethod('ensureHiddenInput', null);
1764    api.ensureHiddenInputElement = bindMethod('ensureHiddenInputElement', null);
1765    api.setSingleHiddenValue = bindMethod('setSingleHiddenValue', null);
1766    api.removeLocationArrayInputs = bindMethod('removeLocationArrayInputs', undefined);
1767    api.clearCommittedLocationContext = bindMethod('clearCommittedLocationContext', undefined);
1768    api.applySelectionToJqueryForm = bindMethod('applySelectionToJqueryForm', { cityId: '', mode: 'empty' });
1769    api.buildSelectionRequestData = bindMethod('buildSelectionRequestData', { cityId: '', mode: 'empty', ajaxData: {} });
1770    api.hasLocationFiltersInParams = bindMethod('hasLocationFiltersInParams', false);
1771    api.hasMapBoundsValue = bindMethod('hasMapBoundsValue', false);
1772    api.shouldDropCityForBounds = bindMethod('shouldDropCityForBounds', false);
1773    api.paramObjLocksHomeListToMapViewport = bindMethod('paramObjLocksHomeListToMapViewport', false);
1774    api.clearHomeMapIntentFlags = bindMethod('clearHomeMapIntentFlags', undefined);
1775    api.setHomeListIntent = bindMethod('setHomeListIntent', '');
1776    api.getHomeListNavigationState = bindMethod('getHomeListNavigationState', null);
1777})(window);
1778
1779// ---- END: js/components/search-location-form-utils.js ----
1780
1781// ---- START: js/components/deal-type-dd-shared.js ----
1782/* Centralized deal-type (.deal-type-dd) nice-select: maps, UI sync, type-field sync, full-page redirect helper. */
1783(function (window, document) {
1784    'use strict';
1785
1786    var root = window.MirelaSearch = window.MirelaSearch || {};
1787    var api = root.dealTypeDd = root.dealTypeDd || {};
1788
1789    var SEL = {
1790        option: '.deal-type-dd .group-select .nice-select .option',
1791        niceSelectRoot: '.deal-type-dd .nice-select'
1792    };
1793
1794    var DATA_TO_TYPE = { villa: '1', studio: '2', office: '3', house: '4' };
1795    var TYPE_TO_DATA = { '1': 'villa', '2': 'studio', '3': 'office', '4': 'house' };
1796    var DATA_TO_PATH = {
1797        villa: '/prodazhbi',
1798        studio: '/naemi',
1799        office: '/kupuvachi',
1800        house: '/naemateli'
1801    };
1802
1803    api.selectors = SEL;
1804    api.getTypeForDataValue = function (v) { return DATA_TO_TYPE[String(v)] || ''; };
1805    api.getDataValueForType = function (t) { return TYPE_TO_DATA[String(t)] || ''; };
1806    api.getPathSegmentForDataValue = function (v) { return DATA_TO_PATH[String(v)] || ''; };
1807
1808    function normalizeDealTypeKey(raw) {
1809        var s = String(raw || '').trim();
1810        if (Object.prototype.hasOwnProperty.call(DATA_TO_PATH, s)) return s;
1811        var fromNum = TYPE_TO_DATA[s];
1812        if (fromNum && Object.prototype.hasOwnProperty.call(DATA_TO_PATH, fromNum)) return fromNum;
1813        return '';
1814    }
1815
1816    api.normalizeDealTypeKey = normalizeDealTypeKey;
1817
1818    api.getRoutingRule = function (raw) {
1819        var k = normalizeDealTypeKey(raw);
1820        if (!k) return null;
1821        return {
1822            key: k,
1823            pathSegment: DATA_TO_PATH[k],
1824            type: DATA_TO_TYPE[k],
1825            action: (k === 'office' || k === 'house') ? 'redirect' : 'submit'
1826        };
1827    };
1828
1829    api.getCanonicalNumericType = function (raw) {
1830        var k = normalizeDealTypeKey(raw);
1831        return k ? (DATA_TO_TYPE[k] || '') : '';
1832    };
1833
1834    function setCurrentFromOption(dd, optionEl) {
1835        var current = dd && dd.querySelector ? dd.querySelector('.current') : null;
1836        if (!current || !optionEl) return;
1837        var labelEl = optionEl.querySelector ? optionEl.querySelector('.deal-type-label') : null;
1838        var label = String((labelEl || optionEl).textContent || '').trim();
1839        var icon = optionEl.querySelector ? optionEl.querySelector('.deal-type-icon') : null;
1840        if (!icon) {
1841            current.textContent = label;
1842            return;
1843        }
1844        current.textContent = '';
1845        var currentIcon = icon.cloneNode(false);
1846        currentIcon.setAttribute('alt', '');
1847        currentIcon.setAttribute('aria-hidden', 'true');
1848        currentIcon.className = 'deal-type-icon';
1849        var currentLabel = document.createElement('span');
1850        currentLabel.className = 'deal-type-current-label';
1851        currentLabel.textContent = label;
1852        currentLabel.setAttribute('title', label);
1853        current.appendChild(currentIcon);
1854        current.appendChild(currentLabel);
1855    }
1856
1857    api.setCurrentFromOption = setCurrentFromOption;
1858
1859    function getFilterStateSnapshot() {
1860        try {
1861            if (typeof window.filterState === 'object' && window.filterState) {
1862                return window.filterState;
1863            }
1864        } catch (_eFilterState) {
1865            return {};
1866        }
1867        return {};
1868    }
1869
1870    function getCentralType() {
1871        try {
1872            var snapshot = getFilterStateSnapshot();
1873            var normalized = String(snapshot.type || '').trim();
1874            if (normalized === '1' || normalized === '2' || normalized === '3' || normalized === '4') {
1875                return normalized;
1876            }
1877        } catch (_eSlice) {}
1878        return '';
1879    }
1880
1881    function getCurrentTypeFromForm(formNative) {
1882        try {
1883            var form = formNative || document.querySelector('.js-search-form');
1884            if (form && form.querySelector) {
1885                var input = form.querySelector('input[name="type"], select[name="type"], .js-type-input, .js-mobile-type-input');
1886                var formType = String(input && input.value != null ? input.value : '').trim();
1887                if (formType === '1' || formType === '2' || formType === '3' || formType === '4') {
1888                    return formType;
1889                }
1890            }
1891        } catch (_eFormType) {}
1892        return getCentralType();
1893    }
1894
1895    api.applyOptionVisual = function (optionEl, opts) {
1896        opts = opts || {};
1897        var closeDropdown = opts.closeDropdown === true;
1898        try {
1899            var dd = optionEl && optionEl.closest ? optionEl.closest('.nice-select') : null;
1900            if (!dd) return;
1901            dd.querySelectorAll('.option').forEach(function (li) { li.classList.remove('selected'); });
1902            optionEl.classList.add('selected');
1903            setCurrentFromOption(dd, optionEl);
1904            if (closeDropdown) {
1905                dd.classList.remove('open');
1906                var wrap = dd.closest('.deal-type-dd');
1907                if (wrap && wrap.classList.contains('open')) wrap.classList.remove('open');
1908            }
1909        } catch (_e) {}
1910    };
1911
1912    api.setTypeInputValue = function (typeNum, opts) {
1913        opts = opts || {};
1914        var next = String(typeNum || '').trim();
1915        if (!next) return;
1916        try {
1917            if (typeof window.changeTypeState === 'function') {
1918                window.changeTypeState(next);
1919            }
1920        } catch (_eStateFn) {}
1921        try {
1922            document.querySelectorAll('input[name="type"], select[name="type"], .js-type-input, .js-mobile-type-input').forEach(function (el) {
1923                if (!el) return;
1924                if (opts.onlyIfEmpty === true && String(el.value || '').trim() !== '') return;
1925                el.value = next;
1926                try {
1927                    el.dispatchEvent(new Event('change', { bubbles: true }));
1928                } catch (_eDispatch) {}
1929            });
1930        } catch (_eDom) {}
1931        try {
1932            var $ = window.jQuery;
1933            if (!$) return;
1934            $('input[name="type"], select[name="type"], .js-type-input, .js-mobile-type-input').each(function () {
1935                if (opts.onlyIfEmpty === true && String($(this).val() || '').trim() !== '') return;
1936                $(this).val(next).trigger('change');
1937            });
1938        } catch (_eJq) {}
1939    };
1940
1941    api.setJqueryTypeInputValue = function (typeNum) {
1942        api.setTypeInputValue(typeNum);
1943    };
1944
1945    api.syncInitialNiceSelectFromDom = function (opts) {
1946        opts = opts || {};
1947        var dd = document.querySelector(opts.rootSelector || SEL.niceSelectRoot);
1948        if (!dd) return;
1949        var centralType = getCentralType();
1950        if (centralType) {
1951            api.syncAllNiceSelectsForNumericType(centralType);
1952            if (opts.fillEmptyTypeInput !== false) {
1953                api.setTypeInputValue(centralType, { onlyIfEmpty: true });
1954            }
1955            return;
1956        }
1957        var selected = dd.querySelector('.option.selected') || dd.querySelector('.option');
1958        if (selected && !selected.classList.contains('selected')) selected.classList.add('selected');
1959        if (selected) setCurrentFromOption(dd, selected);
1960        if (opts.fillEmptyTypeInput === false) return;
1961        var val = selected ? selected.getAttribute('data-value') : 'villa';
1962        var typeStr = DATA_TO_TYPE[val] || '1';
1963        api.setTypeInputValue(typeStr, { onlyIfEmpty: true });
1964    };
1965
1966    api.syncAllNiceSelectsForNumericType = function (typeNum) {
1967        var normalizedType = String(typeNum || '').trim();
1968        try { window.__mobileModalDealTypeChoice = normalizedType; } catch (_eChoice) {}
1969        var dv = TYPE_TO_DATA[normalizedType];
1970        if (!dv) return;
1971        try {
1972            document.querySelectorAll('.deal-type-dd .nice-select').forEach(function (ns) {
1973                ns.querySelectorAll('.option').forEach(function (o) {
1974                    o.classList.remove('selected');
1975                    if (o.getAttribute('data-value') === dv) {
1976                        o.classList.add('selected');
1977                        setCurrentFromOption(ns, o);
1978                    }
1979                });
1980            });
1981        } catch (_e) {}
1982    };
1983
1984    function serializeFormExcluding($form, excludeKeys) {
1985        excludeKeys = excludeKeys || [];
1986        try {
1987            var fq = root.formQueryUtils;
1988            if (fq && typeof fq.serializeJqueryForm === 'function') {
1989                return fq.serializeJqueryForm($form, { remove: excludeKeys });
1990            }
1991        } catch (_e) {}
1992        try {
1993            var $ = window.jQuery;
1994            if (!$form || !$form.serializeArray) return '';
1995            return $form.serializeArray()
1996                .filter(function (i) {
1997                    return i && i.name !== '_token' && excludeKeys.indexOf(i.name) === -1 && String(i.value || '').trim() !== '';
1998                })
1999                .map(function (i) { return encodeURIComponent(i.name) + '=' + encodeURIComponent(i.value); })
2000                .join('&');
2001        } catch (_e2) {
2002            return '';
2003        }
2004    }
2005
2006    api.serializeFormExcluding = serializeFormExcluding;
2007
2008    function getFormHandles(opts) {
2009        opts = opts || {};
2010        var getForm = typeof opts.getForm === 'function'
2011            ? opts.getForm
2012            : function () {
2013                return window.jQuery ? window.jQuery('.js-search-form') : null;
2014            };
2015        var jqForm = null;
2016        try {
2017            jqForm = getForm();
2018        } catch (_eGetForm) {
2019            jqForm = null;
2020        }
2021        var formNative = null;
2022        try {
2023            if (jqForm && jqForm.jquery) {
2024                formNative = jqForm.get(0) || null;
2025            } else if (jqForm && jqForm.nodeType === 1) {
2026                formNative = jqForm;
2027                jqForm = null;
2028            }
2029        } catch (_eNative) {}
2030        if (!formNative) {
2031            try {
2032                formNative = document.querySelector('.js-search-form');
2033            } catch (_eDom) {}
2034        }
2035        return {
2036            formNative: formNative || null,
2037            formJquery: (jqForm && jqForm.jquery) ? jqForm : null
2038        };
2039    }
2040
2041    function handleNavigationResult(navDeal) {
2042        if (!navDeal || !navDeal.handled) return false;
2043        try {
2044            if (!navDeal.sync && navDeal.promise && typeof navDeal.promise.catch === 'function') {
2045                navDeal.promise.catch(function () {});
2046            }
2047        } catch (_eNavCatch) {}
2048        return true;
2049    }
2050
2051    function defaultNavigateFromForm(formNative) {
2052        try {
2053            var ms = window.MirelaSearch;
2054            var navDeal =
2055                ms && typeof ms.tryOffersSearchServerListNavigationFromForm === 'function' && formNative
2056                    ? ms.tryOffersSearchServerListNavigationFromForm(formNative, {
2057                          removeKeys: ['page']
2058                      })
2059                    : null;
2060            return handleNavigationResult(navDeal);
2061        } catch (_eNav) {
2062            return false;
2063        }
2064    }
2065
2066    api.handleSelection = function (optionEl, opts) {
2067        opts = opts || {};
2068        if (!optionEl) return false;
2069        var val = String(optionEl.getAttribute('data-value') || '').trim();
2070        var rule = api.getRoutingRule(val);
2071        if (!rule || !rule.pathSegment) return false;
2072
2073        var formHandles = getFormHandles(opts);
2074        var ctx = {
2075            optionEl: optionEl,
2076            value: val,
2077            rule: rule,
2078            typeNum: String(rule.type || api.getCanonicalNumericType(val) || '1'),
2079            previousType: getCurrentTypeFromForm(formHandles.formNative),
2080            formNative: formHandles.formNative,
2081            formJquery: formHandles.formJquery,
2082            mode: String(opts.mode || '').trim().toLowerCase() || 'navigate'
2083        };
2084
2085        api.applyOptionVisual(optionEl, { closeDropdown: opts.closeDropdown !== false });
2086        api.setTypeInputValue(ctx.typeNum, { dispatchDomEvents: true, forceNotify: true });
2087
2088        if (typeof opts.onBeforeAction === 'function') {
2089            try {
2090                if (opts.onBeforeAction(ctx) === false) return true;
2091            } catch (_eBefore) {}
2092        }
2093
2094        if (ctx.mode === 'list-ajax') {
2095            if (typeof opts.onListAjax === 'function') {
2096                try {
2097                    opts.onListAjax(ctx);
2098                    return true;
2099                } catch (_eList) {
2100                    return true;
2101                }
2102            }
2103            return true;
2104        }
2105
2106        if (typeof opts.onNavigate === 'function') {
2107            try {
2108                var navResult = opts.onNavigate(ctx);
2109                if (navResult === true) return true;
2110                if (navResult && typeof navResult === 'object') {
2111                    return handleNavigationResult(navResult);
2112                }
2113            } catch (_eCustomNav) {}
2114        }
2115
2116        return defaultNavigateFromForm(ctx.formNative);
2117    };
2118
2119    api.installSelectionRouting = function (opts) {
2120        opts = opts || {};
2121        var key = String(opts.bindingKey || opts.mode || 'default').trim() || 'default';
2122        window.__mirelaDealTypeDdBindings = window.__mirelaDealTypeDdBindings || {};
2123        if (window.__mirelaDealTypeDdBindings[key]) return;
2124        window.__mirelaDealTypeDdBindings[key] = true;
2125
2126        document.addEventListener('click', function (ev) {
2127            var opt = ev && ev.target && ev.target.closest ? ev.target.closest(SEL.option) : null;
2128            if (!opt) return;
2129            if (typeof opts.shouldHandle === 'function') {
2130                try {
2131                    if (opts.shouldHandle(opt, ev) === false) return;
2132                } catch (_eShouldHandle) {}
2133            }
2134            try {
2135                ev.preventDefault();
2136                ev.stopPropagation();
2137                if (ev.stopImmediatePropagation) ev.stopImmediatePropagation();
2138            } catch (_eCancel) {}
2139            api.handleSelection(opt, opts);
2140        }, true);
2141    };
2142
2143    api.installFullPageRedirectListener = function (opts) {
2144        opts = opts || {};
2145        opts.mode = 'navigate';
2146        opts.bindingKey = opts.bindingKey || 'full-page';
2147        api.installSelectionRouting(opts);
2148    };
2149
2150    if (!window.__mirelaDealTypeDdDomSyncBound) {
2151        window.__mirelaDealTypeDdDomSyncBound = true;
2152        // Bubble (not capture): installSelectionRouting handlers use capture + stopPropagation,
2153        // so they run first with an uncorrupted previous type. Capture-phase sync here ran before
2154        // late-bound routing (e.g. demands-submit) and broke price-clear transitions (4→3).
2155        document.addEventListener('click', function (ev) {
2156            var opt = ev && ev.target && ev.target.closest ? ev.target.closest(SEL.option) : null;
2157            if (!opt) return;
2158            api.applyOptionVisual(opt, { closeDropdown: false });
2159            api.setTypeInputValue(api.getCanonicalNumericType(opt.getAttribute('data-value')) || '1', { dispatchDomEvents: true, forceNotify: true });
2160        });
2161    }
2162})(window, document);
2163
2164// ---- END: js/components/deal-type-dd-shared.js ----
2165
2166// ---- START: js/components/mirela-search-call-utils.js ----
2167/* Safe resolve/call helpers for MirelaSearch modules. */
2168(function (window) {
2169    'use strict';
2170
2171    var root = window.MirelaSearch = window.MirelaSearch || {};
2172    var api = root.callUtils = root.callUtils || {};
2173
2174    function getModule(name) {
2175        try {
2176            var key = (name == null) ? '' : String(name).trim();
2177            if (!key) return null;
2178            var mod = root[key];
2179            return (mod && typeof mod === 'object') ? mod : null;
2180        } catch (_e) {
2181            return null;
2182        }
2183    }
2184
2185    function call(moduleName, methodName, args, fallback) {
2186        try {
2187            var mod = getModule(moduleName);
2188            var method = (mod && methodName != null) ? mod[String(methodName)] : null;
2189            if (typeof method === 'function') {
2190                var list = Array.isArray(args) ? args : [];
2191                return method.apply(mod, list);
2192            }
2193        } catch (_e) {}
2194        return fallback;
2195    }
2196
2197    function callBool(moduleName, methodName, args, fallback) {
2198        return !!call(moduleName, methodName, args, !!fallback);
2199    }
2200
2201    api.getModule = getModule;
2202    api.call = call;
2203    api.callBool = callBool;
2204})(window);
2205
2206
2207// ---- END: js/components/mirela-search-call-utils.js ----
2208
2209// ---- START: js/api/send-map-browse-offers.js ----
2210'use strict';
2211
2212(function (window) {
2213    'use strict';
2214
2215    var root = window.MirelaSearch = window.MirelaSearch || {};
2216    var api = root.mapBrowseOffersApi = root.mapBrowseOffersApi || {};
2217
2218    function trim(value) {
2219        return String(value == null ? '' : value).trim();
2220    }
2221
2222    function cloneEntries(entries) {
2223        return (Array.isArray(entries) ? entries : []).reduce(function (acc, entry) {
2224            if (!entry || !entry.name) return acc;
2225            acc.push({
2226                name: String(entry.name),
2227                value: String(entry.value == null ? '' : entry.value)
2228            });
2229            return acc;
2230        }, []);
2231    }
2232
2233    function normalizePayload(input) {
2234        if (typeof input === 'string') {
2235            return trim(input);
2236        }
2237        if (Array.isArray(input)) {
2238            return cloneEntries(input);
2239        }
2240        if (input && typeof input === 'object') {
2241            var payload = {};
2242            Object.keys(input).forEach(function (key) {
2243                if (!trim(key)) return;
2244                payload[key] = input[key];
2245            });
2246            return payload;
2247        }
2248        return '';
2249    }
2250
2251    function appendEntry(target, name, value) {
2252        var normalizedName = trim(name);
2253        var normalizedValue = value == null ? '' : String(value);
2254        if (!normalizedName || !trim(normalizedValue)) return;
2255        target.push({ name: normalizedName, value: normalizedValue });
2256    }
2257
2258    function appendArrayEntries(target, name, list) {
2259        (Array.isArray(list) ? list : []).forEach(function (value) {
2260            appendEntry(target, name + '[]', value);
2261        });
2262    }
2263
2264    function appendMultiValueEntries(target, name, value) {
2265        if (Array.isArray(value)) {
2266            appendArrayEntries(target, name, value);
2267            return;
2268        }
2269        var raw = trim(value);
2270        if (!raw) return;
2271        if (raw.indexOf(',') !== -1 || raw.indexOf('|') !== -1) {
2272            raw.split(/[|,]/).forEach(function (item) {
2273                appendEntry(target, name + '[]', item);
2274            });
2275            return;
2276        }
2277        appendEntry(target, name + '[]', raw);
2278    }
2279
2280    function buildPayloadFromState(options) {
2281        var opts = options || {};
2282        var filterSnapshot = (typeof window.filterState === 'object' && window.filterState) ? window.filterState : null;
2283        var builder = root.searchRequestBuilder;
2284        var entries = [];
2285
2286        if (builder && typeof builder.serializeSnapshotToEntries === 'function') {
2287            entries = builder.serializeSnapshotToEntries(filterSnapshot, '') || [];
2288        } else if (filterSnapshot && typeof filterSnapshot === 'object') {
2289            [
2290                'type', 's', 'city_id', 'street_id',
2291                'price_from', 'price_to', 'area_from', 'area_to',
2292                'floor_from', 'floor_to', 'is_top_floor', 'bedrooms', 'bathrooms',
2293                'sea_side', 'has_upkeep', 'yearround_use',
2294                'personal', 'has_project', 'new_building', 'any_parking',
2295                'no_transition', 'guard', 'elevator', 'furnishing',
2296                'no_furnishing', 'pets_allowed', 'map', 'map_manual', 'zoom'
2297            ].forEach(function (key) {
2298                appendEntry(entries, key, filterSnapshot[key]);
2299            });
2300            [
2301                'district_id',
2302                'district_group_id',
2303                'property_type',
2304                'focus',
2305                'construction_type',
2306                'construction_stage',
2307                'land_only'
2308            ].forEach(function (key) {
2309                appendMultiValueEntries(entries, key, filterSnapshot[key]);
2310            });
2311        }
2312
2313        if (!entries.length) {
2314            return '';
2315        }
2316
2317        if (opts.page != null) {
2318            appendEntry(entries, 'page', opts.page);
2319        }
2320
2321        if (opts.extraEntries && typeof opts.extraEntries === 'object') {
2322            if (Array.isArray(opts.extraEntries)) {
2323                entries = entries.concat(cloneEntries(opts.extraEntries));
2324            } else {
2325                Object.keys(opts.extraEntries).forEach(function (key) {
2326                    var value = opts.extraEntries[key];
2327                    if (Array.isArray(value)) {
2328                        value.forEach(function (item) {
2329                            appendEntry(entries, /\[\]$/.test(key) ? key : key + '[]', item);
2330                        });
2331                        return;
2332                    }
2333                    appendEntry(entries, key, value);
2334                });
2335            }
2336        }
2337
2338        return entries;
2339    }
2340
2341    function mapBrowseOffersEndpoint() {
2342        try {
2343            var m = String((window.location && window.location.pathname) || '')
2344                .match(/^\/(en|ru|de)(?=\/|$)/i);
2345            return (m ? '/' + m[1].toLowerCase() : '') + '/map-browse-offers';
2346        } catch (_e) {
2347            return '/map-browse-offers';
2348        }
2349    }
2350
2351    function sendMapBrowseOffers(options) {
2352        var opts = options || {};
2353        var hasExplicitPayload =
2354            Object.prototype.hasOwnProperty.call(opts, 'payload') &&
2355            opts.payload !== undefined &&
2356            opts.payload !== null;
2357        var payload = normalizePayload(
2358            hasExplicitPayload ? opts.payload : buildPayloadFromState(opts)
2359        );
2360
2361        if (typeof window.jQuery === 'undefined' || typeof window.jQuery.post !== 'function') {
2362            return null;
2363        }
2364
2365        var xhr;
2366        if (typeof opts.success === 'function') {
2367            xhr = window.jQuery.post(mapBrowseOffersEndpoint(), payload, opts.success, opts.dataType);
2368        } else {
2369            xhr = window.jQuery.post(mapBrowseOffersEndpoint(), payload);
2370        }
2371
2372        if (xhr && typeof xhr.fail === 'function' && typeof opts.fail === 'function') {
2373            xhr.fail(opts.fail);
2374        }
2375        if (xhr && typeof xhr.always === 'function' && typeof opts.always === 'function') {
2376            xhr.always(opts.always);
2377        }
2378        return xhr;
2379    }
2380
2381    api.buildPayloadFromState = buildPayloadFromState;
2382    api.sendMapBrowseOffers = sendMapBrowseOffers;
2383    window.sendMapBrowseOffers = sendMapBrowseOffers;
2384})(window);
2385
2386// ---- END: js/api/send-map-browse-offers.js ----
2387
2388// ---- START: js/api/map-browse-renderers.js ----
2389'use strict';
2390
2391(function (window) {
2392    'use strict';
2393
2394    function noop() {}
2395    var managedMapLoader = null;
2396
2397    function callMaybe(fn, response, context) {
2398        if (typeof fn !== 'function') return undefined;
2399        return fn(response, context);
2400    }
2401
2402    function send(options, onSuccess) {
2403        var opts = options || {};
2404        if (typeof window.sendMapBrowseOffers !== 'function') {
2405            return null;
2406        }
2407        return window.sendMapBrowseOffers({
2408            payload: opts.payload,
2409            mapString: opts.mapString,
2410            page: opts.page,
2411            extraEntries: opts.extraEntries,
2412            dataType: opts.dataType,
2413            success: onSuccess,
2414            fail: opts.fail,
2415            always: opts.always
2416        });
2417    }
2418
2419    function normalizeBrowseResponseForMapRender(response) {
2420        if (!response || typeof response !== 'object') return response;
2421        try {
2422            if (
2423                response.marker_data &&
2424                Array.isArray(response.marker_data) &&
2425                (!response.data || !Array.isArray(response.data))
2426            ) {
2427                return Object.assign({}, response, { data: response.marker_data });
2428            }
2429        } catch (_e) {}
2430        return response;
2431    }
2432
2433    function defaultPinsMoveMapRender(response) {
2434        var ml = window.MapLeaflet;
2435        if (!ml || typeof ml.render !== 'function') return;
2436        var resp = normalizeBrowseResponseForMapRender(response);
2437        try {
2438            ml.render(resp);
2439        } catch (_eRender) {}
2440        if (typeof ml.fitMapBoundsSafely === 'function') {
2441            try {
2442                ml.fitMapBoundsSafely(resp);
2443            } catch (_eFit) {}
2444        }
2445    }
2446
2447    function showMapBrowseLoader() {
2448        try {
2449            if (managedMapLoader) return;
2450            var jq = window.jQuery;
2451            if (!jq) return;
2452            var container = jq('.b-browse-map').first();
2453            if (!container.length || container.find('.overlay-loading').length) return;
2454            managedMapLoader = jq("<div class='overlay-loading' data-map-browse-render-loader='1'><div class='preload-logo'><div class='spinner'></div></div></div>").css(
2455                'pointer-events',
2456                'none'
2457            );
2458            container.append(managedMapLoader);
2459        } catch (_e) {}
2460    }
2461
2462    function hideMapBrowseLoader() {
2463        try {
2464            if (!managedMapLoader) return;
2465            managedMapLoader.remove();
2466            managedMapLoader = null;
2467        } catch (_e) {
2468            managedMapLoader = null;
2469        }
2470    }
2471
2472    function normalizeRenderOptions(options) {
2473        if (!options || typeof options !== 'object') return {};
2474        return options;
2475    }
2476
2477    /** Shared page contract from filter-state.blade.php: mapOnly | mapAndList | submit */
2478    function resolveBrowsePageType() {
2479        var pt = '';
2480        try {
2481            pt = String(window.pageType || '').trim();
2482        } catch (_e) {
2483            pt = '';
2484        }
2485        if (pt === 'mapOnly' || pt === 'mapAndList' || pt === 'submit') {
2486            return pt;
2487        }
2488        return 'submit';
2489    }
2490
2491    /**
2492     * Centralized auto-browse dispatcher for state-driven filter/location commits.
2493     * - mapOnly: pins only
2494     * - mapAndList: list entrypoint (which renders both map and list through renderFullMapBrowse path)
2495     * - submit: no-op
2496     */
2497    function dispatchBrowseByPageType(options) {
2498        var opts = normalizeRenderOptions(options);
2499        var pt = opts.pageType || resolveBrowsePageType();
2500        if (pt === 'submit') {
2501            return null;
2502        }
2503        if (pt === 'mapOnly') {
2504            return renderPinsMoveMap(opts.mapOnlyOptions || opts);
2505        }
2506        if (pt === 'mapAndList') {
2507            if (typeof opts.mapAndListAction === 'function') {
2508                return opts.mapAndListAction();
2509            }
2510            if (typeof window.loadOffers === 'function') {
2511                try {
2512                    if (typeof window.resetPaginationFilterState === 'function') {
2513                        window.resetPaginationFilterState();
2514                    }
2515                } catch (_eResetPage) {}
2516                return window.loadOffers(1, false, true);
2517            }
2518        }
2519        return null;
2520    }
2521
2522    function renderPinsMoveMap(options) {
2523        var opts = normalizeRenderOptions(options);
2524        var isStandaloneCall = !opts.payload && typeof opts.renderMap !== 'function';
2525        if (isStandaloneCall && opts.defer !== false) {
2526            window.setTimeout(function () {
2527                renderPinsMoveMap(Object.assign({}, opts, { defer: false }));
2528            }, 0);
2529            return null;
2530        }
2531
2532        var renderMap = typeof opts.renderMap === 'function' ? opts.renderMap : defaultPinsMoveMapRender;
2533        if (opts.loader !== false) {
2534            showMapBrowseLoader();
2535        }
2536
2537        var xhr = send(Object.assign({}, opts, {
2538            always: function () {
2539                if (opts.loader !== false) {
2540                    hideMapBrowseLoader();
2541                }
2542                callMaybe(opts.always, null, { source: opts.source || 'map' });
2543            }
2544        }), function (response) {
2545            var context = {
2546                payload: opts.payload,
2547                source: opts.source || 'map'
2548            };
2549            if (callMaybe(opts.beforeRender, response, context) === false) {
2550                return;
2551            }
2552            callMaybe(renderMap || noop, response, context);
2553            callMaybe(opts.afterRender, response, context);
2554        });
2555        if (!xhr && opts.loader !== false) {
2556            hideMapBrowseLoader();
2557        }
2558        return xhr;
2559    }
2560
2561    function renderFullMapBrowse(options) {
2562        var opts = options || {};
2563        return send(opts, function (response) {
2564            var context = {
2565                payload: opts.payload,
2566                source: opts.source || 'list'
2567            };
2568            var renderMap = opts.renderMap !== false ? opts.renderMap : null;
2569            var renderList = opts.renderList !== false ? opts.renderList : null;
2570            if (callMaybe(opts.beforeRender, response, context) === false) {
2571                return;
2572            }
2573            if (opts.renderMapFirst === false) {
2574                callMaybe(renderList || noop, response, context);
2575                callMaybe(renderMap || noop, response, context);
2576            } else {
2577                callMaybe(renderMap || noop, response, context);
2578                callMaybe(renderList || noop, response, context);
2579            }
2580            callMaybe(opts.afterRender, response, context);
2581        });
2582    }
2583
2584    window.renderPinsMoveMap = renderPinsMoveMap;
2585    window.renderFullMapBrowse = renderFullMapBrowse;
2586    window.dispatchBrowseByPageType = dispatchBrowseByPageType;
2587})(window);
2588
2589// ---- END: js/api/map-browse-renderers.js ----
2590
2591// ---- START: js/components/map-browse-offers-ajax.js ----
2592'use strict';
2593
2594function makeOffersAjax(success, dataType) {
2595    try {
2596        if (typeof window.sendMapBrowseOffers === 'function') {
2597            return window.sendMapBrowseOffers({
2598                success: success,
2599                dataType: dataType
2600            });
2601        }
2602    } catch (_e) {}
2603    return null;
2604}
2605
2606// ---- END: js/components/map-browse-offers-ajax.js ----
2607
2608// ---- START: js/components/search-page-context.js ----
2609/* Single place for home / list / list-shell detection used by search bar, hydrator, and submit flows. */
2610(function (window, document) {
2611    'use strict';
2612
2613    var root = window.MirelaSearch = window.MirelaSearch || {};
2614    var route = root.routeUtils || {};
2615
2616    function getRoute() {
2617        return root.routeUtils || route;
2618    }
2619
2620    function getSearchShell() {
2621        try {
2622            var shell = window.__MIRELA_SEARCH_SHELL__;
2623            return (shell && typeof shell === 'object') ? shell : {};
2624        } catch (_e) {
2625            return {};
2626        }
2627    }
2628
2629    function shellFlag(name) {
2630        try {
2631            var shell = getSearchShell();
2632            return shell && shell[name] === true;
2633        } catch (_e) {
2634            return false;
2635        }
2636    }
2637
2638    function hasServerShell() {
2639        try {
2640            return !!String(getSearchShell().pageKind || '').trim();
2641        } catch (_e) {
2642            return false;
2643        }
2644    }
2645
2646    function isListRoutePath(path) {
2647        try {
2648            var r = getRoute();
2649            if (r && typeof r.isListRoutePath === 'function') {
2650                return !!r.isListRoutePath(path);
2651            }
2652        } catch (_e) {}
2653        var p = String(path || '').toLowerCase();
2654        return /^\/(?:en\/|ru\/|de\/)?(?:prodazhbi|naemi|kupuvachi|naemateli|for-sale|for-rent|prodazha|arenda|verkauf|miete)(?:\/|$)/.test(p);
2655    }
2656
2657    function detectLocale(pathname) {
2658        try {
2659            var r = getRoute();
2660            if (r && typeof r.detectLocaleFromPath === 'function') {
2661                return String(r.detectLocaleFromPath(pathname || window.location.pathname || '') || 'bg').toLowerCase() || 'bg';
2662            }
2663        } catch (_e) {}
2664        var first = String(String(pathname || window.location.pathname || '').split('/').filter(Boolean)[0] || '').toLowerCase();
2665        if (first === 'en' || first === 'ru' || first === 'de') return first;
2666        return 'bg';
2667    }
2668
2669    /** Offer preview uses list-shaped SEO paths; exclude from list URL authority and list-only UX. */
2670    function isOfferPreviewBlockingListContext() {
2671        if (shellFlag('isPreview')) return true;
2672        try {
2673            if (window.isOfferPreviewPage === true) return true;
2674        } catch (_e) {}
2675        return false;
2676    }
2677
2678    function isOfferLikeListShapedPath(path) {
2679        try {
2680            var r = getRoute();
2681            if (r && typeof r.isOfferLikePath === 'function') {
2682                return !!r.isOfferLikePath(path != null ? path : window.location.pathname || '');
2683            }
2684        } catch (_e) {}
2685        return false;
2686    }
2687
2688    /** True when list XHR entry exists (window.loadOffers or searchRequest.loadOffersImpl). */
2689    function listAjaxRuntimeReady() {
2690        try {
2691            var lr = root.listRuntime;
2692            if (lr && typeof lr.hasAjaxLoadOffers === 'function') {
2693                return !!lr.hasAjaxLoadOffers();
2694            }
2695        } catch (_eLr) {}
2696        try {
2697            return typeof window.loadOffers === 'function';
2698        } catch (_e2) {
2699            return false;
2700        }
2701    }
2702
2703    /**
2704     * List shell for hydration / canonical URL rules (runs before loadOffers may exist).
2705     * Path matches known list SEO routes, or SSR flag, or layout + AJAX runtime.
2706     * Excludes offer-detail URLs that share the /{deal}/… prefix with lists.
2707     */
2708    function isListSearchContext() {
2709        if (hasServerShell()) return shellFlag('isList');
2710        if (isOfferPreviewBlockingListContext()) return false;
2711        var path = String(window.location.pathname || '');
2712        if (isOfferLikeListShapedPath(path)) return false;
2713        try {
2714            if (window.isListPage === true) return true;
2715        } catch (_e) {}
2716        try {
2717            if (document.querySelector('.wrap-left') && listAjaxRuntimeReady()) return true;
2718        } catch (_e2) {}
2719        return isListRoutePath(path);
2720    }
2721
2722    function isListSearchContextEarly() {
2723        if (hasServerShell()) return shellFlag('isList');
2724        if (isOfferPreviewBlockingListContext()) return false;
2725        var pathEarly = String(window.location.pathname || '');
2726        if (isOfferLikeListShapedPath(pathEarly)) return false;
2727        try {
2728            if (window.isListPage === true) return true;
2729        } catch (_e) {}
2730        try {
2731            if (document.querySelector('.wrap-left')) return true;
2732        } catch (_e2) {}
2733        return isListRoutePath(pathEarly);
2734    }
2735
2736    /** True when /map-browse-offers via loadOffers is allowed. */
2737    function isListAjaxAvailable() {
2738        try {
2739            if (window.__disableAjaxList) return false;
2740        } catch (_e) {}
2741        try {
2742            return !!(document.querySelector('.wrap-left') && listAjaxRuntimeReady());
2743        } catch (_e2) {}
2744        return false;
2745    }
2746
2747    /**
2748     * List page shell: left column present and either AJAX list or non-AJAX list refresh mode.
2749     * Use for ribbon submit / top-types instead of ad-hoc checks.
2750     */
2751    function isListShellWithRefresh() {
2752        try {
2753            var scShell = root.searchSubmitController || null;
2754            if (scShell && typeof scShell.isExternalSearchPage === 'function' && scShell.isExternalSearchPage()) {
2755                return false;
2756            }
2757        } catch (_eExtShell) {}
2758        try {
2759            var hasLayout = !!document.querySelector('.wrap-left');
2760            if (!hasLayout) return false;
2761            var hasRuntime = listAjaxRuntimeReady() || !!window.__disableAjaxList;
2762            return !!hasRuntime;
2763        } catch (_e) {
2764            return false;
2765        }
2766    }
2767
2768    function isHomePage() {
2769        if (hasServerShell()) return shellFlag('isHome');
2770        try {
2771            if (document.querySelector('.wrap-left')) return false;
2772            var path = window.location.pathname || '';
2773            if (isListRoutePath(path)) return false;
2774            if ((path === '/' || path === '' || /^\/(en|de|ru)\/?$/.test(path)) && !!document.querySelector('#map')) {
2775                return true;
2776            }
2777            return false;
2778        } catch (_e) {
2779            return false;
2780        }
2781    }
2782
2783    /**
2784     * "List map" page for submit controller and navigation: home is never list.
2785     * Accepts list shell before loadOffers attaches if URL is a list route or ListPageConfig is present.
2786     */
2787    function isListMapPage() {
2788        if (hasServerShell()) return shellFlag('isList');
2789        try {
2790            if (isHomePage()) return false;
2791        } catch (_eH) {}
2792        var pathLm = String(window.location.pathname || '');
2793        if (isOfferLikeListShapedPath(pathLm)) return false;
2794        try {
2795            if (window.isListPage === true) return true;
2796        } catch (_eW) {}
2797        try {
2798            if (!document.querySelector('.wrap-left')) return false;
2799        } catch (_eL) {
2800            return false;
2801        }
2802        try {
2803            var path = pathLm;
2804            var onListPath = isListRoutePath(path);
2805            var hasMap = !!document.querySelector('#map');
2806            if (!hasMap && !onListPath && !window.ListPageConfig) return false;
2807            if (listAjaxRuntimeReady()) return true;
2808            if (window.__disableAjaxList) return true;
2809            if (window.ListPageConfig) return true;
2810            if (onListPath) return true;
2811            return false;
2812        } catch (_e) {
2813            return false;
2814        }
2815    }
2816
2817    function getSearchProfileName() {
2818        try {
2819            if (isHomePage()) return 'home_map';
2820        } catch (_eHome) {}
2821        try {
2822            if (isListAjaxAvailable()) return 'list_ajax';
2823        } catch (_eListAjax) {}
2824        try {
2825            if (isListShellWithRefresh()) return 'list_submit_only';
2826        } catch (_eListShell) {}
2827        try {
2828            if (window.isDemandsPage === true) return 'demand_list_submit';
2829        } catch (_eDemand) {}
2830        return 'external_submit';
2831    }
2832
2833    function getBodyLocationPageKind() {
2834        try {
2835            if (!document.body || !document.body.classList) return '';
2836            if (document.body.classList.contains('page-home')) return 'home';
2837            if (document.body.classList.contains('page-list')) return 'list';
2838            if (document.body.classList.contains('page-preview')) return 'preview';
2839            if (document.body.classList.contains('page-demands')) return 'demands';
2840        } catch (_e) {}
2841        return '';
2842    }
2843
2844    function getLegacyMarkupPageKind() {
2845        try {
2846            var LEG = window.MirelaLocationLegacyOrchestration;
2847            return LEG && typeof LEG.resolvePageKind === 'function'
2848                ? String(LEG.resolvePageKind() || '').trim().toLowerCase()
2849                : '';
2850        } catch (_e) {
2851            return '';
2852        }
2853    }
2854
2855    /**
2856     * Single authoritative page kind for location/search chrome (SSR shell first, then markup, then runtime probes).
2857     * @returns {'home'|'list'|'preview'|'demands'|'other'}
2858     */
2859    function resolveLocationPageKind() {
2860        var bodyPk = getBodyLocationPageKind();
2861        if (bodyPk) return bodyPk;
2862
2863        var legPk = getLegacyMarkupPageKind();
2864        if (legPk === 'preview' || legPk === 'demands' || legPk === 'list' || legPk === 'home') return legPk;
2865
2866        try {
2867            var shell = getSearchShell();
2868            var spk = String(shell.pageKind || '').trim().toLowerCase();
2869            if (spk === 'preview' || spk === 'demands' || spk === 'list' || spk === 'home') return spk;
2870            if (shell.isPreview === true) return 'preview';
2871            if (shell.isDemands === true) return 'demands';
2872            if (shell.isList === true) return 'list';
2873            if (shell.isHome === true) return 'home';
2874        } catch (_eShell) {}
2875
2876        try {
2877            if (isOfferPreviewBlockingListContext()) return 'preview';
2878        } catch (_ePv) {}
2879
2880        try {
2881            if (window.isDemandsPage === true) return 'demands';
2882        } catch (_eDm) {}
2883
2884        try {
2885            if (isListSearchContext()) return 'list';
2886        } catch (_eLi) {}
2887
2888        try {
2889            if (isHomePage()) return 'home';
2890        } catch (_eHm) {}
2891
2892        return 'other';
2893    }
2894
2895    function isHomeOnlyBrowse() {
2896        try {
2897            return !!isHomePage() && !isListSearchContext();
2898        } catch (_e) {
2899            return false;
2900        }
2901    }
2902
2903    var api = root.pageContext = root.pageContext || {};
2904
2905    api.getSearchShell = getSearchShell;
2906    api.hasServerShell = hasServerShell;
2907    api.isListRoutePath = isListRoutePath;
2908    api.detectLocale = detectLocale;
2909    api.isOfferPreviewBlockingListContext = isOfferPreviewBlockingListContext;
2910    api.isListSearchContext = isListSearchContext;
2911    api.isListSearchContextEarly = isListSearchContextEarly;
2912    api.isListAjaxAvailable = isListAjaxAvailable;
2913    api.isListShellWithRefresh = isListShellWithRefresh;
2914    api.isHomePage = isHomePage;
2915    api.isListMapPage = isListMapPage;
2916    api.isOfferLikeListShapedPath = isOfferLikeListShapedPath;
2917    api.getSearchProfileName = getSearchProfileName;
2918
2919    var pageKindApi = root.pageKind = root.pageKind || {};
2920    pageKindApi.resolve = resolveLocationPageKind;
2921    pageKindApi.isHomeOnlyBrowse = isHomeOnlyBrowse;
2922})(window, document);
2923
2924// ---- END: js/components/search-page-context.js ----
2925
2926// ---- START: js/components/search-runtime-state.js ----
2927/* Central runtime state for shared search/list/home flags. */
2928(function (window, document) {
2929    'use strict';
2930
2931    var root = window.MirelaSearch = window.MirelaSearch || {};
2932    var api = root.runtimeState = root.runtimeState || {};
2933    var state = api.__state || (api.__state = {});
2934
2935    function hasOwn(obj, key) {
2936        return Object.prototype.hasOwnProperty.call(obj, key);
2937    }
2938
2939    function emitChange(key, value, previousValue) {
2940        try {
2941            document.dispatchEvent(new CustomEvent('mirela:runtime-state-change', {
2942                detail: {
2943                    key: String(key || ''),
2944                    value: value,
2945                    previousValue: previousValue
2946                }
2947            }));
2948        } catch (_e) {}
2949    }
2950
2951    function get(key, fallback) {
2952        var name = String(key || '');
2953        if (!name) return fallback;
2954        if (hasOwn(state, name)) return state[name];
2955        if (hasOwn(window, name)) return window[name];
2956        return fallback;
2957    }
2958
2959    function set(key, value, opts) {
2960        var name = String(key || '');
2961        if (!name) return value;
2962        var options = opts || {};
2963        var previousValue = get(name);
2964        state[name] = value;
2965        if (options.mirrorWindow !== false) {
2966            try { window[name] = value; } catch (_e) {}
2967        }
2968        emitChange(name, value, previousValue);
2969        return value;
2970    }
2971
2972    function getFlag(key) {
2973        return get(key) === true;
2974    }
2975
2976    function setFlag(key, enabled, opts) {
2977        return set(key, enabled === true, opts);
2978    }
2979
2980    function clear(key, opts) {
2981        var name = String(key || '');
2982        if (!name) return false;
2983        var options = opts || {};
2984        var previousValue = get(name);
2985        try { delete state[name]; } catch (_e) { state[name] = undefined; }
2986        if (options.clearWindow !== false) {
2987            try { delete window[name]; }
2987 catch (_e2) { try { window[name] = undefined; } catch (_e3) {} }
2988        }
2989        emitChange(name, undefined, previousValue);
2990        return true;
2991    }
2992
2993    function snapshot() {
2994        var out = {};
2995        try {
2996            Object.keys(state).forEach(function (key) {
2997                out[key] = state[key];
2998            });
2999        } catch (_e) {}
3000        return out;
3001    }
3002
3003    api.get = get;
3004    api.set = set;
3005    api.getFlag = getFlag;
3006    api.setFlag = setFlag;
3007    api.clear = clear;
3008    api.snapshot = snapshot;
3009})(window, document);
3010
3011// ---- END: js/components/search-runtime-state.js ----
3012
3013// ---- START: js/components/search-debug.js ----
3014/* Development-only debug logger for shared search flow. */
3015(function (window) {
3016    'use strict';
3017
3018    var root = window.MirelaSearch = window.MirelaSearch || {};
3019    var api = root.debug = root.debug || {};
3020
3021    function isEnabled() {
3022        try {
3023            if (window.__MIRELA_SEARCH_DEBUG__ === true) return true;
3024        } catch (_e) {}
3025        try {
3026            var host = String(window.location.hostname || '').toLowerCase();
3027            return host === 'localhost' ||
3028                host === '127.0.0.1' ||
3029                /\.test$/.test(host) ||
3030                /\.local$/.test(host);
3031        } catch (_e2) {
3032            return false;
3033        }
3034    }
3035
3036    function log(scope, message, payload) {
3037        if (!isEnabled()) return;
3038        try {
3039            if (payload === undefined) {
3040                console.debug('[MirelaSearch][' + String(scope || 'general') + '] ' + String(message || ''));
3041                return;
3042            }
3043            console.debug('[MirelaSearch][' + String(scope || 'general') + '] ' + String(message || ''), payload);
3044        } catch (_e) {}
3045    }
3046
3047    function warn(scope, message, errOrPayload) {
3048        if (!isEnabled()) return;
3049        try {
3050            console.warn('[MirelaSearch][' + String(scope || 'general') + '] ' + String(message || ''), errOrPayload);
3051        } catch (_e) {}
3052    }
3053
3054    api.isEnabled = isEnabled;
3055    api.log = log;
3056    api.warn = warn;
3057})(window);
3058
3059// ---- END: js/components/search-debug.js ----
3060
3061// ---- START: js/components/search-agent-path-utils.js ----
3062/* Shared helpers for SEO agent listing paths -> canonical normal list paths. */
3063(function (window) {
3064    'use strict';
3065
3066    var root = window.MirelaSearch = window.MirelaSearch || {};
3067    var api = root.agentPathUtils = root.agentPathUtils || {};
3068
3069    function inferDealTypeFromAgentListingPath(pathname) {
3070        try {
3071            var p = String(pathname || '').toLowerCase();
3072            if (p.indexOf('imoti-pod-naem-na-agent-') !== -1 || p.indexOf('properties-for-rent-by-agent-') !== -1 || p.indexOf('immobilien-zur-miete-von-agent-') !== -1 || p.indexOf('nedvizhimost-v-arendu-u-agenta-') !== -1) return '2';
3073            if (p.indexOf('imoti-za-prodazhba-na-agent-') !== -1 || p.indexOf('properties-for-sale-by-agent-') !== -1 || p.indexOf('immobilien-zum-verkauf-von-agent-') !== -1 || p.indexOf('nedvizhimost-na-prodazhu-u-agenta-') !== -1) return '1';
3074            if (p.indexOf('zayavki-ot-kupuvachi-na-agent-') !== -1 || p.indexOf('buyer-requests-by-agent-') !== -1 || p.indexOf('kaufanfragen-bei-agent-') !== -1 || p.indexOf('zayavki-ot-pokupateley-u-agenta-') !== -1) return '3';
3075            if (p.indexOf('zayavki-ot-naemateli-na-agent-') !== -1 || p.indexOf('tenant-requests-by-agent-') !== -1 || p.indexOf('mietanfragen-bei-agent-') !== -1 || p.indexOf('zayavki-ot-arendatorov-u-agenta-') !== -1) return '4';
3076        } catch (_e) {}
3077        return '';
3078    }
3079
3080    function detectLocale(pathname) {
3081        try {
3082            var route = root.routeUtils || null;
3083            if (route && typeof route.detectLocaleFromPath === 'function') {
3084                return String(route.detectLocaleFromPath(pathname || '') || 'bg').toLowerCase();
3085            }
3086        } catch (_e) {}
3087        return 'bg';
3088    }
3089
3090    function getDealSegment(locale, typeValue) {
3091        try {
3092            var builder = root.listUrlBuilder || null;
3093            if (builder && typeof builder.getDealSegment === 'function') {
3094                return String(builder.getDealSegment(locale, typeValue) || '').trim();
3095            }
3096        } catch (_e) {}
3097        var map = {
3098            bg: { '1': 'prodazhbi', '2': 'naemi', '3': 'kupuvachi', '4': 'naemateli' },
3099            en: { '1': 'for-sale', '2': 'for-rent', '3': 'kupuvachi', '4': 'naemateli' },
3100            ru: { '1': 'prodazha', '2': 'arenda', '3': 'kupuvachi', '4': 'naemateli' },
3101            de: { '1': 'verkauf', '2': 'miete', '3': 'kupuvachi', '4': 'naemateli' }
3102        };
3103        var l = String(locale || 'bg').toLowerCase();
3104        var t = String(typeValue || '1');
3105        return (map[l] && map[l][t]) || map.bg[t] || 'prodazhbi';
3106    }
3107
3108    function buildCanonicalListPathFromAgentPath(pathname, explicitType) {
3109        try {
3110            var locale = detectLocale(pathname || '');
3111            var typeValue = String(explicitType || inferDealTypeFromAgentListingPath(pathname || '') || '').trim();
3112            if (!/^[1-4]$/.test(typeValue)) return '';
3113            var segment = getDealSegment(locale, typeValue);
3114            if (!segment) return '';
3115            return (locale === 'bg' ? '' : ('/' + locale)) + '/' + segment;
3116        } catch (_e) {
3117            return '';
3118        }
3119    }
3120
3121    var AGENT_LISTING_PREFIX_MAP = {
3122        bg: {
3123            '1': 'imoti-za-prodazhba-na-agent',
3124            '2': 'imoti-pod-naem-na-agent',
3125            '3': 'zayavki-ot-kupuvachi-na-agent',
3126            '4': 'zayavki-ot-naemateli-na-agent'
3127        },
3128        en: {
3129            '1': 'properties-for-sale-by-agent',
3130            '2': 'properties-for-rent-by-agent',
3131            '3': 'buyer-requests-by-agent',
3132            '4': 'tenant-requests-by-agent'
3133        },
3134        ru: {
3135            '1': 'nedvizhimost-na-prodazhu-u-agenta',
3136            '2': 'nedvizhimost-v-arendu-u-agenta',
3137            '3': 'zayavki-ot-pokupateley-u-agenta',
3138            '4': 'zayavki-ot-arendatorov-u-agenta'
3139        },
3140        de: {
3141            '1': 'immobilien-zum-verkauf-von-agent',
3142            '2': 'immobilien-zur-miete-von-agent',
3143            '3': 'kaufanfragen-bei-agent',
3144            '4': 'mietanfragen-bei-agent'
3145        }
3146    };
3147
3148    // Given the current agent SEO path, return the agent path for another deal type
3149    // (e.g. /imoti-za-prodazhba-na-agent-… → /imoti-pod-naem-na-agent-…). Empty if not applicable.
3150    function buildAgentListingPathForType(pathname, typeNum) {
3151        try {
3152            if (!isAgentListingPath(pathname)) return '';
3153            var pathOnly = String(pathname || '').split('?')[0] || '';
3154            var locale = detectLocale(pathname || '');
3155            var parts = pathOnly.split('/').filter(Boolean);
3156            if (!parts.length) return '';
3157            var idx = locale === 'bg' ? 0 : 1;
3158            if (idx >= parts.length) return '';
3159            var currentSegment = String(parts[idx] || '').trim();
3160            if (!currentSegment) return '';
3161            var suffix = '';
3162            Object.keys(AGENT_LISTING_PREFIX_MAP).some(function (mapLocale) {
3163                var localeMap = AGENT_LISTING_PREFIX_MAP[mapLocale] || {};
3164                return Object.keys(localeMap).some(function (mapType) {
3165                    var prefix = String(localeMap[mapType] || '').trim();
3166                    var token = prefix ? (prefix + '-') : '';
3167                    if (!token || currentSegment.indexOf(token) !== 0) return false;
3168                    suffix = currentSegment.slice(prefix.length);
3169                    return true;
3170                });
3171            });
3172            var targetMap = AGENT_LISTING_PREFIX_MAP[locale] || AGENT_LISTING_PREFIX_MAP.bg;
3173            var nextPrefix = String(targetMap[String(typeNum || '').trim()] || '').trim();
3174            if (!suffix || !nextPrefix) return '';
3175            parts[idx] = nextPrefix + suffix;
3176            return '/' + parts.join('/');
3177        } catch (_e) {
3178            return '';
3179        }
3180    }
3181
3182    function isAgentListingPath(pathname) {
3183        try {
3184            var pathOnly = String(pathname || '').split('?')[0] || '';
3185            var segments = pathOnly.split('/').filter(Boolean);
3186            if (!segments.length) return false;
3187            var idx = 0;
3188            var first = String(segments[0] || '').toLowerCase();
3189            if (first === 'bg' || first === 'en' || first === 'de' || first === 'ru') {
3190                idx = 1;
3191            }
3192            if (!segments[idx]) return false;
3193            var slugSegment = String(segments[idx] || '');
3194            if (!/-\d+$/.test(slugSegment)) return false;
3195            var prefixes = [
3196                'imoti-za-prodazhba-na-agent',
3197                'imoti-pod-naem-na-agent',
3198                'zayavki-ot-kupuvachi-na-agent',
3199                'zayavki-ot-naemateli-na-agent',
3200                'properties-for-sale-by-agent',
3201                'properties-for-rent-by-agent',
3202                'buyer-requests-by-agent',
3203                'tenant-requests-by-agent',
3204                'immobilien-zum-verkauf-von-agent',
3205                'immobilien-zur-miete-von-agent',
3206                'kaufanfragen-bei-agent',
3207                'mietanfragen-bei-agent',
3208                'nedvizhimost-na-prodazhu-u-agenta',
3209                'nedvizhimost-v-arendu-u-agenta',
3210                'zayavki-ot-pokupateley-u-agenta',
3211                'zayavki-ot-arendatorov-u-agenta'
3212            ];
3213            for (var i = 0; i < prefixes.length; i++) {
3214                if (slugSegment.indexOf(prefixes[i] + '-') === 0) return true;
3215            }
3216        } catch (_e2) {}
3217        return false;
3218    }
3219
3220    api.inferDealTypeFromAgentListingPath = inferDealTypeFromAgentListingPath;
3221    api.buildCanonicalListPathFromAgentPath = buildCanonicalListPathFromAgentPath;
3222    api.isAgentListingPath = isAgentListingPath;
3223    api.buildAgentListingPathForType = buildAgentListingPathForType;
3224})(window);
3225
3226// ---- END: js/components/search-agent-path-utils.js ----
3227
3228// ---- START: js/components/advanced-filters-panel-controls.js ----
3229/* Advanced filters panel open/close controls and safety guards. */
3230(function (window, document) {
3231    'use strict';
3232
3233    function removeBackdropsAndModalState() {
3234        try {
3235            document.querySelectorAll('.modal-backdrop, .offcanvas-backdrop').forEach(function (b) {
3236                try { b.parentNode && b.parentNode.removeChild(b); } catch (_e) {}
3237            });
3238            document.body.classList.remove('modal-open');
3239            document.body.style.removeProperty('padding-right');
3240        } catch (_e2) {}
3241    }
3242
3243    function cleanupBlockingOverlays() {
3244        try {
3245            document.querySelectorAll('.modal.show').forEach(function (m) {
3246                try {
3247                    if (window.bootstrap && window.bootstrap.Modal) {
3248                        var inst = window.bootstrap.Modal.getInstance(m) || window.bootstrap.Modal.getOrCreateInstance(m);
3249                        if (inst && inst.hide) inst.hide();
3250                    }
3251                } catch (_e2) {}
3252                try { m.classList.remove('show'); } catch (_eModalClass) {}
3253            });
3254            document.querySelectorAll('.offcanvas.show').forEach(function (oc) {
3255                try {
3256                    if (window.bootstrap && window.bootstrap.Offcanvas) {
3257                        var oi = window.bootstrap.Offcanvas.getInstance(oc) || window.bootstrap.Offcanvas.getOrCreateInstance(oc);
3258                        if (oi && oi.hide) oi.hide();
3259                    }
3260                } catch (_e3) {}
3261                try { oc.classList.remove('show'); } catch (_e4) {}
3262            });
3263        } catch (_e5) {}
3264        removeBackdropsAndModalState();
3265    }
3266
3267    function setHomeOfferTabsZIndex(zIndexValue) {
3268        try {
3269            var tabs = document.querySelector('.home-offer-tabs');
3270            if (tabs) tabs.style.setProperty('z-index', String(zIndexValue), 'important');
3271        } catch (_e) {}
3272    }
3273
3274    function setHeaderZIndexForFiltersOpen(isOpen) {
3275        try {
3276            if (window.innerWidth >= 1100) return;
3277            var hdr = document.querySelector('.main-header.fixed-header');
3278            if (!hdr) return;
3279            if (isOpen) {
3280                hdr.style.setProperty('z-index', '-15', 'important');
3281            } else {
3282                var st = window.pageYOffset || document.documentElement.scrollTop || 0;
3283                var onHome = document.body && document.body.classList.contains('page-home');
3284                var z = onHome ? '10100' : (st > 250 ? '9999' : '999');
3285                hdr.style.setProperty('z-index', z, 'important');
3286            }
3287        } catch (_e2) {}
3288    }
3289
3290    function setMobileSearchWrapHidden(hidden) {
3291        try {
3292            var inp = document.querySelector('.js-mobile-search-input');
3293            var wrap = inp && inp.closest('.flex-grow-1');
3294            if (!wrap) return;
3295            if (hidden) wrap.style.setProperty('display', 'none', 'important');
3296            else wrap.style.removeProperty('display');
3297        } catch (_e) {}
3298    }
3299
3300    function syncFiltersOpenClass(show) {
3301        try {
3302            if (window.innerWidth < 1100) {
3303                if (show) document.body.classList.add('filters-open');
3304                else document.body.classList.remove('filters-open');
3305            }
3306        } catch (_e) {}
3307    }
3308
3309    function isAdvancedPanelOpen() {
3310        try {
3311            var form = document.querySelector('.wd-search-form');
3312            return !!(form && form.classList.contains('show'));
3313        } catch (_e) {
3314            return false;
3315        }
3316    }
3317
3318    function toggleAdvFilters(show) {
3319        const form = document.querySelector('.wd-search-form');
3320        if (!form) return;
3321
3322        if (show) {
3323            cleanupBlockingOverlays();
3324
3325            form.classList.add('show');
3326            setHomeOfferTabsZIndex(1000);
3327            setHeaderZIndexForFiltersOpen(true);
3328        } else {
3329            form.classList.remove('show');
3330            setHomeOfferTabsZIndex(100003);
3331            setHeaderZIndexForFiltersOpen(false);
3332        }
3333
3334        syncFiltersOpenClass(show);
3335    }
3336
3337    function attachAdvancedToggleListeners() {
3338        if (document.__mirelaAdvancedToggleBound) return;
3339        document.__mirelaAdvancedToggleBound = true;
3340
3341        document.addEventListener('click', function (e) {
3342            var target = e && e.target && e.target.closest
3343                ? e.target.closest('.filter-advanced, #toggleAdvFilters, .js-mobile-filter-btn')
3344                : null;
3345            if (!target) return;
3346            if (e.target && e.target.closest && e.target.closest('.filter-advanced-clear, [aria-label="Изчисти"]')) {
3347                return;
3348            }
3349            try { e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); } catch (_) {}
3350
3351            var shouldOpen = !isAdvancedPanelOpen();
3352            if (window.MobileFilters && typeof window.MobileFilters.open === 'function' && typeof window.MobileFilters.close === 'function') {
3353                if (shouldOpen) window.MobileFilters.open();
3354                else window.MobileFilters.close();
3355                return;
3356            }
3357            toggleAdvFilters(shouldOpen);
3358        }, true);
3359
3360        var closeBtn = document.getElementById('js-mobile-close-filters');
3361        if (closeBtn) {
3362            if (closeBtn.__mirela_mobile_close_filters_bound) {
3363                closeBtn.removeEventListener('click', closeBtn.__mirela_mobile_close_filters_bound);
3364            }
3365            closeBtn.__mirela_mobile_close_filters_bound = function () {
3366                if (window.MobileFilters && typeof window.MobileFilters.close === 'function') {
3367                    window.MobileFilters.close();
3368                } else {
3369                    toggleAdvFilters(false);
3370                }
3371            };
3372            closeBtn.addEventListener('click', closeBtn.__mirela_mobile_close_filters_bound);
3373        }
3374    }
3375
3376    function restoreUiAfterClose() {
3377        try {
3378            setHomeOfferTabsZIndex(100003);
3379            syncFiltersOpenClass(false);
3380            removeBackdropsAndModalState();
3381            setHeaderZIndexForFiltersOpen(false);
3382            setMobileSearchWrapHidden(false);
3383            try {
3384                document.querySelectorAll('.wrap-filter-search').forEach(function (el) {
3385                    el.removeAttribute('style');
3386                });
3387            } catch (_e3) {}
3388        } catch (_e4) {}
3389    }
3390
3391    function applyUiOnOpen() {
3392        try {
3393            setHomeOfferTabsZIndex(1000);
3394            setHeaderZIndexForFiltersOpen(true);
3395            syncFiltersOpenClass(true);
3396            setMobileSearchWrapHidden(true);
3397            try { document.dispatchEvent(new CustomEvent('filters-panel-opened')); } catch (_e2) {}
3398        } catch (_e3) {}
3399    }
3400
3401    function initObserver() {
3402        try {
3403            var panel = document.querySelector('.wd-search-form');
3404            if (!panel) return;
3405            var obs = new MutationObserver(function (muts) {
3406                muts.forEach(function (m) {
3407                    if (m.attributeName === 'class') {
3408                        var shown = panel.classList.contains('show');
3409                        if (shown) applyUiOnOpen();
3410                        else restoreUiAfterClose();
3411                    }
3412                });
3413            });
3414            obs.observe(panel, { attributes: true, attributeFilter: ['class'] });
3415        } catch (_e) {}
3416    }
3417
3418    function init() {
3419        attachAdvancedToggleListeners();
3420        try {
3421            window.addEventListener('pageshow', function () {
3422                setTimeout(function () {
3423                    restoreUiAfterClose();
3424                }, 0);
3425            });
3426        } catch (_ePs) {}
3427        initObserver();
3428    }
3429
3430    window.toggleAdvFilters = toggleAdvFilters;
3431    window.attachAdvancedToggleListeners = attachAdvancedToggleListeners;
3432
3433    if (document.readyState === 'loading') {
3434        document.addEventListener('DOMContentLoaded', init);
3435    } else {
3436        init();
3437    }
3438
3439    var ms = window.MirelaSearch = window.MirelaSearch || {};
3440    ms.uiShell = ms.uiShell || {};
3441    ms.uiShell.removeBackdropsAndModalState = removeBackdropsAndModalState;
3442    ms.uiShell.cleanupBlockingOverlays = cleanupBlockingOverlays;
3443})(window, document);
3444
3445
3446// ---- END: js/components/advanced-filters-panel-controls.js ----
3447
3448// ---- START: js/components/district-polygons-map-apply.js ----
3449/**
3450 * Shared: apply a /address or /api/locations/polygons style response onto MapLeaflet.
3451 * - preserveMarkers: true → setDistrictPolygons when available (list SEO hydrate, home restore) so pins are not wiped.
3452 * - preserveMarkers: false → MapLeaflet.render({ data: [], ... }) first (home autocomplete / mobile flow).
3453 */
3454(function (window) {
3455    'use strict';
3456
3457    var root = (window.MirelaSearch = window.MirelaSearch || {});
3458
3459    /**
3460     * @param {object} ml MapLeaflet API
3461     * @param {{ district_polygons?: object, bounds?: object }} resp
3462     * @param {{ preserveMarkers?: boolean, markerData?: array }} [opts]
3463     * @returns {boolean} true if polygons were applied
3464     */
3465    root.applyDistrictPolygonsToMapLeaflet = function (ml, resp, opts) {
3466        opts = opts || {};
3467        if (!ml || !resp) {
3468            return false;
3469        }
3470        var polys = resp.district_polygons;
3471        if (!polys || typeof polys !== 'object') {
3472            return false;
3473        }
3474        var keys = Object.keys(polys);
3475        if (!keys.length) {
3476            return false;
3477        }
3478        var preserve = opts.preserveMarkers === true;
3479        var markerData = opts.markerData != null ? opts.markerData : [];
3480        try {
3481            if (preserve && typeof ml.setDistrictPolygons === 'function') {
3482                ml.setDistrictPolygons(polys);
3483                return true;
3484            }
3485            if (typeof ml.render === 'function') {
3486                ml.render({
3487                    data: markerData,
3488                    district_polygons: polys,
3489                    bounds: resp.bounds,
3490                });
3491                return true;
3492            }
3493            if (typeof ml.setDistrictPolygons === 'function') {
3494                ml.setDistrictPolygons(polys);
3495                return true;
3496            }
3497        } catch (_e) {}
3498        return false;
3499    };
3500
3501    root.hasNonEmptyDistrictPolygons = function (resp) {
3502        if (!resp || !resp.district_polygons || typeof resp.district_polygons !== 'object') {
3503            return false;
3504        }
3505        return Object.keys(resp.district_polygons).length > 0;
3506    };
3507
3508    root.clearDistrictPolygonsOnMap = function (ml) {
3509        try {
3510            if (ml && typeof ml.setDistrictPolygons === 'function') {
3511                ml.setDistrictPolygons({});
3512            }
3513        } catch (_e) {}
3514    };
3515
3516    /**
3517     * @param {string[]|string|null|undefined} districtIds array or CSV
3518     * @param {string[]|string|null|undefined} groupIds array or CSV
3519     * @returns {Promise<object>} parsed JSON from /api/locations/polygons
3520     */
3521    root.fetchDistrictPolygonsFromApi = function (districtIds, groupIds) {
3522        var usp = new URLSearchParams();
3523        var dStr =
3524            districtIds != null && districtIds !== ''
3525                ? Array.isArray(districtIds)
3526                    ? districtIds
3527                          .map(function (v) {
3528                              return String(v || '').trim();
3529                          })
3530                          .filter(Boolean)
3531                          .join(',')
3532                    : String(districtIds).trim()
3533                : '';
3534        var gStr =
3535            groupIds != null && groupIds !== ''
3536                ? Array.isArray(groupIds)
3537                    ? groupIds
3538                          .map(function (v) {
3539                              return String(v || '').trim();
3540                          })
3541                          .filter(Boolean)
3542                          .join(',')
3543                    : String(groupIds).trim()
3544                : '';
3545        if (dStr) {
3546            usp.set('district_ids', dStr);
3547        }
3548        if (gStr) {
3549            usp.set('group_ids', gStr);
3550        }
3551        var q = usp.toString();
3552        if (!q) {
3553            return Promise.resolve({ district_polygons: {}, bounds: null });
3554        }
3555        return fetch('/api/locations/polygons?' + q, { credentials: 'same-origin' }).then(function (r) {
3556            return r.json();
3557        });
3558    };
3559})(window);
3560
3561// ---- END: js/components/district-polygons-map-apply.js ----
3562
3563// ---- START: js/components/home/map-user-gesture-tracker.js ----
3564/* Home map user gesture tracker. */
3565(function (window, document) {
3566    'use strict';
3567
3568    var root = window.MirelaSearch = window.MirelaSearch || {};
3569    var api = root.homeMapUserGestureTracker = root.homeMapUserGestureTracker || {};
3570
3571    function isHomeOffersCarouselGestureBlocked() {
3572        try {
3573            return window.__mirelaHomeOffersCarouselGesture === true;
3574        } catch (_error) {
3575            return false;
3576        }
3577    }
3578
3579    /**
3580     * Reuse the shared runtime flag API when available, otherwise fall back to
3581     * the legacy globals still consumed by MapLeaflet and home.js.
3582     */
3583    function resolveListRuntimeFlags() {
3584        try {
3585            var mirelaSearch = window.MirelaSearch;
3586            if (!mirelaSearch) return null;
3587            if (mirelaSearch.listRuntimeFlags) return mirelaSearch.listRuntimeFlags;
3588            if (mirelaSearch.listPageRuntimeFlags && typeof mirelaSearch.listPageRuntimeFlags.create === 'function') {
3589                return mirelaSearch.listPageRuntimeFlags.create();
3590            }
3591        } catch (_error) {}
3592        return null;
3593    }
3594
3595    /**
3596     * Mark the map as user-driven after a real drag/zoom gesture.
3597     * The home-specific flag is intentionally set only on the homepage.
3598     */
3599    function markMapAsUserDriven(isHomePage) {
3600        try {
3601            var runtimeFlags = resolveListRuntimeFlags();
3602            if (runtimeFlags && typeof runtimeFlags.setUserMovedMapFlag === 'function') {
3603                runtimeFlags.setUserMovedMapFlag(true);
3604            } else {
3605                window.__mirelaUserMovedMap = true;
3606            }
3607
3608            if (isHomePage !== true) return;
3609
3610            if (runtimeFlags && typeof runtimeFlags.setUserMovedMapHomeFlag === 'function') {
3611                runtimeFlags.setUserMovedMapHomeFlag(true);
3612            } else {
3613                window.__mirelaUserMovedMapHome = true;
3614            }
3615
3616            if (runtimeFlags && typeof runtimeFlags.mirrorNow === 'function') {
3617                runtimeFlags.mirrorNow();
3618            }
3619        } catch (_error) {}
3620    }
3621
3622    /**
3623     * Attach a minimal set of listeners that detect real user interaction with
3624     * the home map. Programmatic moves should not set the flags.
3625     */
3626    function create(options) {
3627        options = options || {};
3628
3629        function isHomePage() {
3630            try {
3631                return typeof options.isHomePage === 'function' && options.isHomePage() === true;
3632            } catch (_error) {
3633                return false;
3634            }
3635        }
3636
3637        function getLeafletMap() {
3638            try {
3639                return (typeof options.getLeafletMap === 'function') ? options.getLeafletMap() : null;
3640            } catch (_error) {
3641                return null;
3642            }
3643        }
3644
3645        function markFromGesture() {
3646            if (isHomeOffersCarouselGestureBlocked()) return;
3647            markMapAsUserDriven(isHomePage());
3648        }
3649
3650        function attachTouchGestureFallback(leafletMap) {
3651            try {
3652                var container = leafletMap && typeof leafletMap.getContainer === 'function'
3653                    ? leafletMap.getContainer()
3654                    : null;
3655                if (!container || container.__mirelaHomeTouchGestureFallbackAttached === true) {
3656                    return;
3657                }
3658                container.__mirelaHomeTouchGestureFallbackAttached = true;
3659
3660                container.addEventListener('touchstart', function (event) {
3661                    try {
3662                        if (!event || !event.touches || event.touches.length < 2) return;
3663                        markFromGesture();
3664                    } catch (_touchStartError) {}
3665                }, { passive: true, capture: true });
3666            } catch (_error) {}
3667        }
3668
3669        function attachZoomControlListener() {
3670            try {
3671                var zoomControls = document.querySelector('.leaflet-control-zoom');
3672                if (!zoomControls) {
3673                    setTimeout(attachZoomControlListener, 100);
3674                    return;
3675                }
3676                zoomControls.addEventListener('click', function (event) {
3677                    var target = event.target;
3678                    if (!target) return;
3679                    if (
3680                        target.classList.contains('leaflet-control-zoom-in') ||
3681                        target.classList.contains('leaflet-control-zoom-out') ||
3682                        target.closest('.leaflet-control-zoom-in') ||
3683                        target.closest('.leaflet-control-zoom-out')
3684                    ) {
3685                        markFromGesture();
3686                    }
3687                }, true);
3688            } catch (_error) {}
3689        }
3690
3691        /**
3692         * Start listening once the Leaflet instance exists.
3693         */
3694        function startTracking() {
3695            try {
3696                if (window.__mirelaHomeMapUserGestureTrackerStarted) return;
3697                window.__mirelaHomeMapUserGestureTrackerStarted = true;
3698
3699                var attachWhenReady = function () {
3700                    try {
3701                        var leafletMap = getLeafletMap();
3702                        if (!leafletMap || !leafletMap.on) {
3703                            setTimeout(attachWhenReady, 250);
3704                            return;
3705                        }
3706
3707                        leafletMap.on('dragstart', function () {
3708                            markFromGesture();
3709                        });
3710
3711                        leafletMap.on('movestart', function (event) {
3712                            if (event && event.originalEvent) {
3713                                markFromGesture();
3714                            }
3715                        });
3716
3717                        leafletMap.on('zoomstart', function (event) {
3718                            if (event && event.originalEvent) {
3719                                markFromGesture();
3720                            }
3721                        });
3722
3723                        leafletMap.on('zoomend', function (event) {
3724                            if (event && event.originalEvent) {
3725                                markFromGesture();
3726                            }
3727                        });
3728
3729                        attachTouchGestureFallback(leafletMap);
3730                        attachZoomControlListener();
3731                    } catch (_error) {}
3732                };
3733
3734                setTimeout(attachWhenReady, 0);
3735            } catch (_error) {}
3736        }
3737
3738        return {
3739            startTracking: startTracking
3740        };
3741    }
3742
3743    api.create = create;
3744})(window, document);
3745
3746// ---- END: js/components/home/map-user-gesture-tracker.js ----
3747
3748// ---- START: js/components/search-canonical-rel-parser.js ----
3749/* Shared parser for canonical list relative URLs -> search/filter state fragments. */
3750(function (window) {
3751    'use strict';
3752
3753    var root = window.MirelaSearch = window.MirelaSearch || {};
3754    var api = root.searchCanonicalRelParser = root.searchCanonicalRelParser || {};
3755
3756    function trim(value) {
3757        return String(value == null ? '' : value).trim();
3758    }
3759
3760    function parseQuery(urlOrQuery) {
3761        try {
3762            var q = urlOrQuery || '';
3763            if (q && q[0] === '/' && q.indexOf('?') === -1) return {};
3764            if (q.indexOf('?') !== -1) q = q.split('?')[1] || '';
3765            var usp = new URLSearchParams(q);
3766            var out = {};
3767            usp.forEach(function (v, k) {
3768                var key = String(k || '');
3769                if (key.slice(-2) === '[]') {
3770                    var arrKey = key.slice(0, -2);
3771                    if (!Array.isArray(out[arrKey])) out[arrKey] = [];
3772                    out[arrKey].push(v);
3773                    return;
3774                }
3775                if (Object.prototype.hasOwnProperty.call(out, key)) {
3776                    if (!Array.isArray(out[key])) out[key] = [out[key]];
3777                    out[key].push(v);
3778                    return;
3779                }
3780                out[key] = v;
3781            });
3782            return out;
3783        } catch (_e) {
3784            return {};
3785        }
3786    }
3787
3788    function getTailConfig() {
3789        try {
3790            return window.__mirelaSearchBarTailConfig || {};
3791        } catch (_e) {
3792            return {};
3793        }
3794    }
3795
3796    function getTopTypesGroups() {
3797        try {
3798            if (window.__mirelaTopTypesConfig && Array.isArray(window.__mirelaTopTypesConfig.groups)) {
3799                return window.__mirelaTopTypesConfig.groups;
3800            }
3801        } catch (_e) {}
3802        var cfg = getTailConfig();
3803        return Array.isArray(cfg.topTypesGroups) ? cfg.topTypesGroups : [];
3804    }
3805
3806    function reverseLookupIdBySlug(slugMap, slug, locale) {
3807        try {
3808            var loc = trim(locale || 'bg').toLowerCase() || 'bg';
3809            var wanted = trim(slug).toLowerCase();
3810            if (!wanted) return '';
3811            var byId = (slugMap && (slugMap[loc] || slugMap.bg)) || {};
3812            var ids = Object.keys(byId);
3813            for (var i = 0; i < ids.length; i++) {
3814                var id = trim(ids[i]);
3815                if (!id) continue;
3816                if (trim(byId[id]).toLowerCase() === wanted) return id;
3817            }
3818        } catch (_e) {}
3819        return '';
3820    }
3821
3822    /** District group slug: try path locale first, then other locales (explicit slug may exist only under bg). */
3823    function reverseLookupDistrictGroupIdAllLocales(slugMap, slug, preferredLocale) {
3824        try {
3825            var wanted = trim(String(slug || '')).toLowerCase();
3826            if (!wanted || !slugMap) return '';
3827            var order = [];
3828            var pl = trim(preferredLocale || 'bg').toLowerCase() || 'bg';
3829            order.push(pl);
3830            ['bg', 'en', 'ru', 'de'].forEach(function (L) {
3831                if (order.indexOf(L) === -1) order.push(L);
3832            });
3833            for (var oi = 0; oi < order.length; oi++) {
3834                var loc = order[oi];
3835                var byId = (slugMap[loc] || slugMap.bg) || {};
3836                var ids = Object.keys(byId).map(function (k) { return trim(k); }).filter(Boolean);
3837                ids.sort(function (a, b) {
3838                    var na = parseInt(a, 10); var nb = parseInt(b, 10);
3839                    if (na !== nb) return na - nb;
3840                    return String(a).localeCompare(String(b));
3841                });
3842                for (var ii = 0; ii < ids.length; ii++) {
3843                    if (trim(String(byId[ids[ii]] || '')).toLowerCase() === wanted) return ids[ii];
3844                }
3845            }
3846        } catch (_e2) {}
3847        return '';
3848    }
3849
3850    /** Compound segment: merge district slug tokens from all locales (path often uses BG tokens on /prodazhbi/...). */
3851    function tryResolveDistrictGroupFromCompoundSeg(seg2, cityId, preferredLocale) {
3852        var cfg = getTailConfig();
3853        var cid = trim(String(cityId || ''));
3854        var wanted = trim(String(seg2 || '')).toLowerCase();
3855        if (!cid || !wanted) return '';
3856
3857        var dgByDist = cfg.districtGroupIdByDistrictId || {};
3858        var membersByGroup = cfg.districtGroupMemberIdsByGroupId || {};
3859        var distCity = cfg.districtCityIdById || {};
3860        var localeOrder = [];
3861        var pl = trim(preferredLocale || 'bg').toLowerCase() || 'bg';
3862        localeOrder.push(pl);
3863        ['bg', 'en', 'ru', 'de'].forEach(function (L) {
3864            if (localeOrder.indexOf(L) === -1) localeOrder.push(L);
3865        });
3866
3867        var slugToId = {};
3868        var ambiguous = false;
3869        localeOrder.forEach(function (loc) {
3870            var slugMap = (cfg.districtSlugById && (cfg.districtSlugById[loc] || cfg.districtSlugById.bg)) || {};
3871            Object.keys(slugMap).forEach(function (idStr) {
3872                if (String(distCity[idStr] || '') !== cid) return;
3873                if (!dgByDist[idStr]) return;
3874                var sl = trim(String(slugMap[idStr] || '')).toLowerCase();
3875                if (!sl) return;
3876                if (Object.prototype.hasOwnProperty.call(slugToId, sl) && String(slugToId[sl]) !== String(idStr)) {
3877                    ambiguous = true;
3878                    return;
3879                }
3880                if (!Object.prototype.hasOwnProperty.call(slugToId, sl)) {
3881                    slugToId[sl] = idStr;
3882                }
3883            });
3884        });
3885        if (ambiguous) return '';
3886
3887        var slugsByLength = Object.keys(slugToId);
3888        slugsByLength.sort(function (a, b) { return b.length - a.length; });
3889
3890        var segment = wanted;
3891        var n = segment.length;
3892        var pos = 0;
3893        var matchedIdStrs = [];
3894        while (pos < n) {
3895            if (segment.charAt(pos) === '-') {
3896                pos++;
3897                continue;
3898            }
3899            var found = null;
3900            for (var si = 0; si < slugsByLength.length; si++) {
3901                var candSlug = slugsByLength[si];
3902                var len = candSlug.length;
3903                if (!len || pos + len > n) continue;
3904                if (segment.slice(pos, pos + len) !== candSlug) continue;
3905                var next = pos + len;
3906                if (next !== n && segment.charAt(next) !== '-') continue;
3907                found = candSlug;
3908                break;
3909            }
3910            if (!found) return '';
3911            matchedIdStrs.push(slugToId[found]);
3912            pos += found.length;
3913        }
3914
3915        var uniq = [];
3916        matchedIdStrs.forEach(function (x) {
3917            var t = trim(String(x));
3918            if (t && uniq.indexOf(t) === -1) uniq.push(t);
3919        });
3920        if (uniq.length < 2) return '';
3921
3922        var g0 = String(dgByDist[uniq[0]] || '');
3923        if (!g0) return '';
3924        for (var ui = 1; ui < uniq.length; ui++) {
3925            if (String(dgByDist[uniq[ui]] || '') !== g0) return '';
3926        }
3927
3928        var members = membersByGroup[g0] || [];
3929        var a = uniq.slice().sort(function (x, y) { return parseInt(x, 10) - parseInt(y, 10); });
3930        var b = members.slice().sort(function (x, y) { return parseInt(String(x), 10) - parseInt(String(y), 10); });
3931        if (a.length !== b.length) return '';
3932        for (var j = 0; j < a.length; j++) {
3933            if (a[j] !== b[j]) return '';
3934        }
3935        return g0;
3936    }
3937
3938    function extractStreetIdFromSlug(slug) {
3939        try {
3940            var match = trim(slug).match(/-(\d+)$/);
3941            return (match && match[1]) ? String(match[1]) : '';
3942        } catch (_e) {
3943            return '';
3944        }
3945    }
3946
3947    function isNbSeoSlug(slug) {
3948        var value = trim(slug).toLowerCase();
3949        if (!value) return false;
3950        try {
3951            if (typeof window !== 'undefined' && typeof window.__mirelaIsNbSeoCitySlug === 'function') {
3952                return !!window.__mirelaIsNbSeoCitySlug(value);
3953            }
3954        } catch (_e) {}
3955        return value === 'novi-sgradi-v-stroej'
3956            || value === 'new-buildings-under-construction'
3957            || value === 'novye-zdaniya-v-stroitelstve'
3958            || value === 'neubauten-im-bau';
3959    }
3960
3961    function parseCanonicalListRel(urlLike) {
3962        var base = parseQuery(urlLike);
3963        try {
3964            var rawRel = trim(urlLike);
3965            if (!rawRel || rawRel[0] !== '/') return base;
3966            var u = new URL(rawRel, window.location.origin);
3967            var route = (root && root.routeUtils) ? root.routeUtils : null;
3968            try {
3969                if (route && typeof route.isOfferLikePath === 'function' && route.isOfferLikePath(u.pathname || '')) {
3970                    return base;
3971                }
3972            } catch (_eOfferLike) {}
3973            var parts = route && typeof route.getPathSegments === 'function'
3974                ? route.getPathSegments(u.pathname || '')
3975                : String(u.pathname || '').split('/').filter(Boolean);
3976            var hasLocale = false;
3977            try {
3978                hasLocale = !!(parts.length && route && typeof route.isLocaleSegment === 'function'
3979                    ? route.isLocaleSegment(parts[0])
3980                    : /^(bg|en|ru|de)$/i.test(String(parts[0] || '')));
3981            } catch (_eLocale) {}
3982            var locale = hasLocale ? trim(parts[0]).toLowerCase() : 'bg';
3983            var dealIdx = hasLocale ? 1 : 0;
3984            var dealSeg = trim(parts[dealIdx]).toLowerCase();
3985            var cfg = getTailConfig();
3986            var citySlug = trim(parts[dealIdx + 1]);
3987            var seg2 = trim(parts[dealIdx + 2]);
3988            var seg3 = trim(parts[dealIdx + 3]);
3989            var seg4 = trim(parts[dealIdx + 4]);
3990            if (isNbSeoSlug(citySlug) && reverseLookupIdBySlug(cfg.citySlugById, seg2, locale)) {
3991                citySlug = trim(parts[dealIdx + 2]);
3992                seg2 = trim(parts[dealIdx + 3]);
3993                seg3 = trim(parts[dealIdx + 4]);
3994                seg4 = trim(parts[dealIdx + 5]);
3995                base.has_project = base.has_project || '1';
3996            }
3997            var topGroups = getTopTypesGroups();
3998            var topSlugToIds = {};
3999            topGroups.forEach(function (group) {
4000                if (!group) return;
4001                var slug = trim(group.slug);
4002                if (!slug) return;
4003                topSlugToIds[slug] = Array.isArray(group.type_ids)
4004                    ? group.type_ids.map(function (id) { return String(id); })
4005                    : [];
4006            });
4007
4008            function idsForTopPathSlug(slug) {
4009                var s = trim(slug || '');
4010                if (!s) return [];
4011                if (topSlugToIds[s]) return topSlugToIds[s].slice();
4012                var sl = s.toLowerCase();
4013                var syn = Array.isArray(cfg.topTypesSlugSynonyms) ? cfg.topTypesSlugSynonyms : [];
4014                for (var si = 0; si < syn.length; si++) {
4015                    var row = syn[si];
4016                    if (!Array.isArray(row) || !row.length) continue;
4017                    var inRow = false;
4018                    for (var ji = 0; ji < row.length; ji++) {
4019                        if (String(row[ji] || '').toLowerCase() === sl) {
4020                            inRow = true;
4021                            break;
4022                        }
4023                    }
4024                    if (!inRow) continue;
4025                    for (var ki = 0; ki < row.length; ki++) {
4026                        var cand = trim(String(row[ki] || ''));
4027                        if (cand && topSlugToIds[cand]) return topSlugToIds[cand].slice();
4028                    }
4029                }
4030                return [];
4031            }
4032
4033            /** Single estate-type slug (e.g. dvustaen-apartament) from DB `etypeSlugById`, not top-type groups. */
4034            function idsForSingleEtypeSlug(slug) {
4035                var id = reverseLookupIdBySlug(cfg.etypeSlugById, slug, locale);
4036                return id ? [String(id)] : [];
4037            }
4038
4039            function resolvePropertyTypeIdsFromPathSlug(slug) {
4040                var fromTop = idsForTopPathSlug(slug);
4041                if (fromTop.length) return fromTop;
4042                return idsForSingleEtypeSlug(slug);
4043            }
4044
4045            var patch = Object.assign({}, base);
4046            var cityId = reverseLookupIdBySlug(cfg.citySlugById, citySlug, locale);
4047            if (cityId && !patch.city_id) patch.city_id = cityId;
4048
4049            var dealType = '';
4050            if (dealSeg === 'prodazhbi' || dealSeg === 'for-sale' || dealSeg === 'prodazha' || dealSeg === 'verkauf') dealType = '1';
4051            else if (dealSeg === 'naemi' || dealSeg === 'for-rent' || dealSeg === 'arenda' || dealSeg === 'miete') dealType = '2';
4052            else if (dealSeg === 'kupuvachi') dealType = '3';
4053            else if (dealSeg === 'naemateli') dealType = '4';
4054            if (dealType && !patch.type) patch.type = dealType;
4055
4056            var districtId = '';
4057            var districtGroupId = '';
4058            var streetId = '';
4059            var propertyTypeIds = [];
4060            if (seg2 === 'street' || seg2 === 'ulitsa' || seg2 === 'strasse'
4061                || seg2 === 'kompleks' || seg2 === 'complex' || seg2 === 'komplex') {
4062                streetId = extractStreetIdFromSlug(seg3);
4063                if (seg4) propertyTypeIds = resolvePropertyTypeIdsFromPathSlug(seg4);
4064                patch.street_namespace = String(seg2 || '').toLowerCase();
4065            } else if (seg2) {
4066                var idsSeg2 = idsForTopPathSlug(seg2);
4067                if (idsSeg2.length) {
4068                    propertyTypeIds = idsSeg2;
4069                } else {
4070                    districtId = reverseLookupIdBySlug(cfg.districtSlugById, seg2, locale);
4071                    if (districtId) {
4072                        if (seg3) propertyTypeIds = resolvePropertyTypeIdsFromPathSlug(seg3);
4073                        var dOwnerCity = trim((cfg.districtCityIdById || {})[districtId] || '');
4074                        if (dOwnerCity) {
4075                            patch.city_id = dOwnerCity;
4076                        }
4077                    } else {
4078                        districtGroupId = reverseLookupDistrictGroupIdAllLocales(cfg.districtGroupSlugById, seg2, locale);
4079                        if (!districtGroupId) {
4080                            districtGroupId = tryResolveDistrictGroupFromCompoundSeg(seg2, cityId, locale);
4081                        }
4082                        if (districtGroupId) {
4083                            if (seg3) propertyTypeIds = resolvePropertyTypeIdsFromPathSlug(seg3);
4084                            var gOwnerCity = trim((cfg.districtGroupCityIdById || {})[districtGroupId] || '');
4085                            if (gOwnerCity) {
4086                                patch.city_id = gOwnerCity;
4087                            }
4088                        } else {
4089                            propertyTypeIds = resolvePropertyTypeIdsFromPathSlug(seg2);
4090                            if (!propertyTypeIds.length && seg3) {
4091                                propertyTypeIds = resolvePropertyTypeIdsFromPathSlug(seg3);
4092                            }
4093                        }
4094                    }
4095                }
4096            }
4097
4098            if (districtId && !patch.district_id) patch.district_id = districtId;
4099            if (districtGroupId && !patch.district_group_id) patch.district_group_id = districtGroupId;
4100            if (streetId && !patch.street_id) patch.street_id = streetId;
4101            if (propertyTypeIds.length && !patch.property_type) patch.property_type = propertyTypeIds.slice();
4102
4103            // New-buildings SEO landing (preserve mode only): implied has_project + default Sofia so ?page=2 does not drop the flag.
4104            var citySlugNb = false;
4105            try {
4106                citySlugNb = typeof window !== 'undefined' && typeof window.__mirelaIsNbSeoCitySlug === 'function'
4107                    && window.__mirelaIsNbSeoCitySlug(citySlug);
4108            } catch (_eNb0) {}
4109            if (!citySlugNb) {
4110                var csn = String(citySlug || '').toLowerCase().trim();
4111                if (csn === 'novi-sgradi-v-stroej' || csn === 'new-buildings-under-construction'
4112                    || csn === 'novye-zdaniya-v-stroitelstve' || csn === 'neubauten-im-bau') {
4113                    citySlugNb = true;
4114                }
4115            }
4116            if (citySlugNb) {
4117                var nbRelPreserve = false;
4118                try {
4119                    nbRelPreserve = typeof window !== 'undefined' && window.__mirelaNbLandingSeoPreserve === true;
4120                } catch (_eNbRel) {}
4121                if (nbRelPreserve) {
4122                    if (!patch.city_id) patch.city_id = '3';
4123                    if (!patch.has_project) patch.has_project = '1';
4124                }
4125            }
4126
4127            return patch;
4128        } catch (_e) {
4129            return base;
4130        }
4131    }
4132
4133    function augmentUrlSearchParams(urlParams, rel) {
4134        try {
4135            var patch = parseCanonicalListRel(rel);
4136            Object.keys(patch || {}
4136).forEach(function (key) {
4137                if (key === 'street_namespace') return;
4138                var value = patch[key];
4139                if (value != null && typeof value === 'object' && !Array.isArray(value)) {
4140                    return;
4141                }
4142                if (Array.isArray(value)) {
4143                    if (urlParams.get(key + '[]') || urlParams.get(key)) return;
4144                    value.forEach(function (item) {
4145                        var v = trim(item);
4146                        if (v) urlParams.append(key + '[]', v);
4147                    });
4148                    return;
4149                }
4150                var scalar = trim(value);
4151                if (!scalar || urlParams.get(key) || urlParams.get(key + '[]')) return;
4152                urlParams.set(key, scalar);
4153            });
4154        } catch (_e) {}
4155        return urlParams;
4156    }
4157
4158    api.parseQuery = parseQuery;
4159    api.getTopTypesGroups = getTopTypesGroups;
4160    api.reverseLookupIdBySlug = reverseLookupIdBySlug;
4161    api.extractStreetIdFromSlug = extractStreetIdFromSlug;
4162    api.parseCanonicalListRel = parseCanonicalListRel;
4163    api.augmentUrlSearchParams = augmentUrlSearchParams;
4164})(window);
4165
4166// ---- END: js/components/search-canonical-rel-parser.js ----
4167
4168// ---- START: js/components/search-form-url-hydrator.js ----
4169/* SSR-first search bootstrap.
4170 *
4171 * PHP renders the initial search state. This file intentionally does not hydrate
4172 * URL/session/last-list values into the form; it only exposes the legacy
4173 * completion/orchestrator signals expected by downstream modules while they are
4174 * migrated to the SSR-first contract.
4175 */
4176(function (window, document) {
4177    'use strict';
4178
4179    function getSearchForm() {
4180        try {
4181            var shared = window.AdvancedSearchShared || null;
4182            if (shared && typeof shared.getSearchForm === 'function') {
4183                var sharedForm = shared.getSearchForm();
4184                if (sharedForm) return sharedForm;
4185            }
4186        } catch (_e) {}
4187        try {
4188            return document.querySelector('.js-search-form');
4189        } catch (_e2) {
4190            return null;
4191        }
4192    }
4193
4194    function syncNiceSelectLabelsFromDom(form) {
4195        function syncOne(name) {
4196            try {
4197                var sel = form && form.querySelector ? form.querySelector('select[name="' + name + '"]') : null;
4198                if (!sel) sel = document.querySelector('select[name="' + name + '"]');
4199                if (!sel) return;
4200                var ns = (typeof window.__mirelaNiceSelectUiRoot === 'function')
4201                    ? window.__mirelaNiceSelectUiRoot(sel)
4202                    : sel.nextElementSibling;
4203                if (!ns || !ns.classList || !ns.classList.contains('nice-select')) return;
4204                var options = sel.options || [];
4205                var selectedOpt = (sel.selectedIndex >= 0) ? options[sel.selectedIndex] : null;
4206                if (!selectedOpt && options.length) selectedOpt = options[0];
4207                if (!selectedOpt) return;
4208                var label = (selectedOpt.text || selectedOpt.textContent || '').trim();
4209                var current = ns.querySelector('.current');
4210                if (current && label) current.textContent = label;
4211
4212                var selectedValue = String(sel.value || '').trim();
4213                Array.from(ns.querySelectorAll('.list .option')).forEach(function (li) {
4214                    li.classList.remove('selected', 'focus');
4215                });
4216                var targetLi = selectedValue
4217                    ? ns.querySelector('.list .option[data-value="' + selectedValue.replace(/"/g, '\\"') + '"]')
4218                    : ns.querySelector('.list .option[data-value=""]') || ns.querySelector('.list .option');
4219                if (targetLi) {
4220                    targetLi.classList.add('selected');
4221                    targetLi.classList.add('focus');
4222                }
4223            } catch (_e) {}
4224        }
4225
4226        try {
4227            ['floor_from', 'floor_to', 'sea_side', 'has_upkeep', 'yearround_use'].forEach(syncOne);
4228        } catch (_e) {}
4229    }
4230
4231    function dispatchHydrationComplete() {
4232        try {
4233            var auth = window.MirelaSearch && window.MirelaSearch.searchHydrationAuthority;
4234            if (auth && typeof auth.dispatchHydrationComplete === 'function') {
4235                auth.dispatchHydrationComplete({ source: 'search-form-ssr-bootstrap' });
4236                return;
4237            }
4238            document.dispatchEvent(new CustomEvent('mirela:search-hydration-complete', {
4239                detail: { source: 'search-form-ssr-bootstrap' }
4240            }));
4241        } catch (_e) {}
4242    }
4243
4244    function boot() {
4245        if (window.__mirelaUrlHydratorRan) return;
4246        try { window.__mirelaUrlHydratorRan = true; } catch (_eRan) {}
4247
4248        var root = window.MirelaSearch = window.MirelaSearch || {};
4249        var orch = root.searchHydrationOrchestrator = root.searchHydrationOrchestrator || {};
4250        orch.primaryHydrationRan = true;
4251        orch.runRefreshFromLastList = orch.runRefreshFromLastList || function () {};
4252        orch.runAfterSeoConfigScript = orch.runAfterSeoConfigScript || function () {};
4253        orch.runPostTailInlinePhases = orch.runPostTailInlinePhases || function () {
4254            try { orch.runRefreshFromLastList(); } catch (_eRefresh) {}
4255            try { orch.runAfterSeoConfigScript(); } catch (_eSeo) {}
4256        };
4257
4258        var form = getSearchForm();
4259        syncNiceSelectLabelsFromDom(form);
4260        dispatchHydrationComplete();
4261    }
4262
4263    boot();
4264})(window, document);
4265
4266// ---- END: js/components/search-form-url-hydrator.js ----
4267
4268// ---- START: js/search-autocomplete.js ----
4269/**
4270 * Search Autocomplete Functionality
4271 * Provides dropdown suggestions for location search
4272 * Works for both home page and list page
4273 */
4274
4275(function($) {
4276    'use strict';
4277
4278    // Configuration
4279    const CONFIG = {
4280        searchDelay: 300,
4281        minSearchLength: 2,
4282        ajaxUrl: '/address',
4283        historyLimits: { search: 5, offer: 10, ac: 9, acA: 9 },
4284        selectors: {
4285            input: '.js-search-input',
4286            suggestions: '.js-search-suggestions',
4287            locationMap: '.js-location-map',
4288            districtId: '.js-district-id',
4289            districtGroupId: '.js-district-group-id',
4290            cityId: '.js-city-id'
4291        },
4292        /** Mark on .js-search-suggestions when list is from local history (toggle open/close on input click). */
4293        suggestionsHistoryClass: 'suggestions-from-history',
4294        /** Clear (×) icon — same SVG as .pt-clear-x in js-search-form filter pills. */
4295        clearXSvg: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
4296    };
4297
4298    /** Ignore keyup-driven refresh/collapse while moving through suggestions with keyboard. */
4299    function isSuggestionNavigationKey(e) {
4300        if (!e) return false;
4301        const key = e.key || '';
4302        if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Enter' || key === 'Escape' || key === 'Tab') {
4303            return true;
4304        }
4305        const code = e.keyCode || e.which || 0;
4306        return code === 38 || code === 40 || code === 13 || code === 27 || code === 9;
4307    }
4308
4309    /** Match app isMobile (< 1100) and mobile-location-modal.js viewport gate. */
4310    function isMobileLocationViewport() {
4311        try {
4312            if (typeof window.__mirelaIsMobileLocationViewport === 'function') {
4313                return window.__mirelaIsMobileLocationViewport();
4314            }
4315        } catch (_e) {}
4316        return (typeof window !== 'undefined') && window.innerWidth < 1100;
4317    }
4318
4319    // Helpers to normalize autocomplete value payloads for stable de-duplication
4320    function acNormalizeValue(val){
4321        try{
4322            const v = (val && typeof val === 'object') ? { ...val } : {};
4323            const offerId = String(v.offer_id != null ? v.offer_id : '').trim();
4324            if (v.type === 'offer' && /^\d+$/.test(offerId)) {
4325                return { type: 'offer', offer_id: offerId };
4326            }
4327            const toStrArr = (x) => {
4328                if (x == null) return [];
4329                if (Array.isArray(x)) return x.map(s=>String(s)).filter(s=>s!==
4330                    '').sort();
4331                const s = String(x);
4332                if (s.indexOf(',') !== -1) return s.split(',').map(t=>t.trim()).filter(t=>t!=='').map(String).sort();
4333                return s ? [s] : [];
4334            };
4335            const out = {};
4336            const c = toStrArr(v.cities);
4337            const d = toStrArr(v.districts);
4338            const g = toStrArr(v.district_groups);
4339            const st = (v.street_id != null && v.street_id !== '') ? String(v.street_id) : '';
4340            if (c.length) out.cities = c;
4341            if (d.length) out.districts = d;
4342            if (g.length) out.district_groups = g;
4343            if (st) out.street_id = st;
4344            return out;
4345        }catch(_e){ return {}; }
4346    }
4347    function acValueSig(val){ try{ return JSON.stringify(acNormalizeValue(val)); }catch(_e){ return '{}'; } }
4348
4349    function makeSearchQuery(intent, options) {
4350        try {
4351            if (typeof window.makeQuery === 'function') {
4352                return window.makeQuery(intent || 'submit', options || {});
4353            }
4354        } catch (_eGlobalQuery) {}
4355        return false;
4356    }
4357
4358    function getSearchShell() {
4359        try {
4360            var shell = window.__MIRELA_SEARCH_SHELL__;
4361            return (shell && typeof shell === 'object') ? shell : {};
4362        } catch (_e) {
4363            return {};
4364        }
4365    }
4366
4367    function getPageContextApi() {
4368        try {
4369            return (window.MirelaSearch && window.MirelaSearch.pageContext) ? window.MirelaSearch.pageContext : null;
4370        } catch (_e) {
4371            return null;
4372        }
4373    }
4374
4375    function isHomePageShell() {
4376        try {
4377            var shell = getSearchShell();
4378            if (String(shell.pageKind || '').trim()) return shell.isHome === true || shell.pageKind === 'home';
4379        } catch (_eShell) {}
4380        try {
4381            var pc = getPageContextApi();
4382            if (pc && typeof pc.isHomePage === 'function') return !!pc.isHomePage();
4383        } catch (_ePc) {}
4384        try { return window.isHomePage === true || !!(document.body && document.body.classList && document.body.classList.contains('page-home')); } catch (_e) { return false; }
4385    }
4386
4387    function isListPageShell() {
4388        try {
4389            var shell = getSearchShell();
4390            if (String(shell.pageKind || '').trim()) return shell.isList === true || shell.pageKind === 'list';
4391        } catch (_eShell) {}
4392        try {
4393            var pc = getPageContextApi();
4394            if (pc && typeof pc.isListSearchContext === 'function') return !!pc.isListSearchContext();
4395            if (pc && typeof pc.isListSearchContextEarly === 'function') return !!pc.isListSearchContextEarly();
4396        } catch (_ePc) {}
4397        try { return window.isListPage === true || !!(document.querySelector('.wrap-left') && typeof window.loadOffers === 'function'); } catch (_e) { return false; }
4398    }
4399
4400    function isPreviewPageShell() {
4401        try {
4402            var shell = getSearchShell();
4403            if (String(shell.pageKind || '').trim()) return shell.isPreview === true || shell.pageKind === 'preview';
4404        } catch (_eShell) {}
4405        try { return window.isOfferPreviewPage === true; } catch (_e) { return false; }
4406    }
4407
4408    function isDemandsPageShell() {
4409        try {
4410            var shell = getSearchShell();
4411            // pageKind is authoritative: preview URLs under /kupuvachi|naemateli/ also set isDemands=true in SSR shell.
4412            if (String(shell.pageKind || '').trim()) return shell.pageKind === 'demands';
4413        } catch (_eShell) {}
4414        try { return window.isDemandsPage === true; } catch (_e) { return false; }
4415    }
4416
4417    function isExternalSearchPageShell() {
4418        try {
4419            const submitController =
4420                window.MirelaSearch && window.MirelaSearch.searchSubmitController
4421                    ? window.MirelaSearch.searchSubmitController
4422                    : null;
4423            return !!(submitController && typeof submitController.isExternalSearchPage === 'function' && submitController.isExternalSearchPage());
4424        } catch (_e) {
4425            return false;
4426        }
4427    }
4428
4429    function isActualOfferPreviewPage() {
4430        try {
4431            if (!isPreviewPageShell()) return false;
4432            const route = (window.MirelaSearch && window.MirelaSearch.routeUtils) ? window.MirelaSearch.routeUtils : null;
4433            if (route && typeof route.isOfferLikePath === 'function') {
4434                return !!route.isOfferLikePath(window.location.pathname || '');
4435            }
4436        } catch (_e) {}
4437        return /\/offer\/\d+(?:\/)?$/i.test(String(window.location.pathname || ''));
4438    }
4439
4440    function isStaticLocationPickShell() {
4441        if (isHomePageShell() || isListPageShell() || isActualOfferPreviewPage()) return false;
4442        return isExternalSearchPageShell() || isDemandsPageShell();
4443    }
4444
4445    function shouldAutoSubmitOnSuggestionPick() {
4446        if (isActualOfferPreviewPage()) return false;
4447        if (isHomePageShell()) return false;
4448        if (isListPageShell()) return false;
4449        try {
4450            if (window.__disableAutoSubmitOnStaticSearch === true) return false;
4451        } catch (_eDis) {}
4452        try {
4453            if (window.__enableAutoSubmitOnNonAjax) return true;
4454        } catch (_eEn) {}
4455        try {
4456            const enterApi = window.MirelaSearch && window.MirelaSearch.searchInputEnterHandler;
4457            if (enterApi && typeof enterApi.isStaticSubmitPage === 'function') {
4458                return !!enterApi.isStaticSubmitPage();
4459            }
4460        } catch (_eEnter) {}
4461        try {
4462            if (window.isOfferPreviewPage === true) return true;
4463        } catch (_ePrev) {}
4464        try {
4465            const ssc = window.MirelaSearch && window.MirelaSearch.searchSubmitController;
4466            if (ssc && typeof ssc.isStaticAutoSubmitEnabled === 'function') {
4467                return !!ssc.isStaticAutoSubmitEnabled();
4468            }
4469        } catch (_eSsc) {}
4470        try {
4471            return !window.__MIRELA_SEARCH_SHELL__;
4472        } catch (_eShell) {
4473            return false;
4474        }
4475    }
4476
4477    function triggerAutoSubmitAfterSuggestionPick(formEl) {
4478        if (!shouldAutoSubmitOnSuggestionPick()) return;
4479        const form = formEl || (function () {
4480            try { return document.querySelector('.js-search-form'); } catch (_e) { return null; }
4481        })();
4482        if (!form) return;
4483        setTimeout(function () {
4484            try {
4485                const ms = window.MirelaSearch;
4486                if (ms && typeof ms.triggerFormSubmitLikeSearch === 'function') {
4487                    ms.triggerFormSubmitLikeSearch(form);
4488                    return;
4489                }
4490            } catch (_eTrig) {}
4491            try {
4492                const enterApi = window.MirelaSearch && window.MirelaSearch.searchInputEnterHandler;
4493                if (enterApi && typeof enterApi.dispatchSyntheticSubmit === 'function') {
4494                    enterApi.dispatchSyntheticSubmit(form);
4495                }
4496            } catch (_eDisp) {}
4497        }, 0);
4498    }
4499
4500    // Offer preview: selecting a location must submit like the price/area ribbons do, i.e. go through
4501    // MirelaSearch.range.queueSubmit (which runs the preview→list redirect from the committed ids).
4502    function triggerPreviewSubmitAfterSuggestionPick(formEl) {
4503        const form = formEl || (function () {
4504            try { return document.querySelector('.js-search-form'); } catch (_e) { return null; }
4505        })();
4506        if (!form) return;
4507        try {
4508            const ms = window.MirelaSearch;
4509            if (ms && ms.range && typeof ms.range.queueSubmit === 'function') {
4510                ms.range.queueSubmit(form);
4511                return;
4512            }
4513        } catch (_eQueue) {}
4514        try {
4515            if (typeof window.__mirelaQueueRangeSubmit === 'function') {
4516                window.__mirelaQueueRangeSubmit(form);
4517                return;
4518            }
4519        } catch (_eQueueGlobal) {}
4520        setTimeout(function () {
4521            try {
4522                const ms2 = window.MirelaSearch;
4523                if (ms2 && typeof ms2.triggerFormSubmitLikeSearch === 'function') {
4524                    ms2.triggerFormSubmitLikeSearch(form);
4525                }
4526            } catch (_eTrig) {}
4527        }, 0);
4528    }
4529
4530    // expose for use in other modules
4531    try{ window.__acNormalize = acNormalizeValue; window.__acSig = acValueSig; }catch(_e){}
4532
4533    function acHistoryEffectiveTs(item, fallbackIndex) {
4534        if (item && typeof item.ts === 'number' && item.ts > 0) { return item.ts; }
4535        return typeof fallbackIndex === 'number' ? fallbackIndex : 0;
4536    }
4537
4538    function acHistoryIsInstructionLabel(label) {
4539        try {
4540            const FS = window.MirelaLocationFormSync;
4541            if (FS && typeof FS.isInstructionLocationLabel === 'function') {
4542                return FS.isInstructionLocationLabel(label);
4543            }
4544        } catch (_e) {}
4545        return false;
4546    }
4547
4548    function acHistoryNormalizeRecord(item) {
4549        const entry = Object.assign({}, item || {});
4550        const rawValue = (entry.value && typeof entry.value === 'object') ? entry.value : {};
4551        const rawOfferId = String(rawValue.offer_id != null ? rawValue.offer_id : '').trim();
4552        const isOfferEntry = rawValue.type === 'offer' && /^\d+$/.test(rawOfferId);
4553        const label = String(entry.label || entry.typeahead || entry.suggestion || (isOfferEntry ? rawOfferId : '')).trim();
4554        if (!isOfferEntry && (label.length < 3 || acHistoryIsInstructionLabel(label))) { return null; }
4555        entry.label = label;
4556        entry.typeahead = label;
4557        entry.suggestion = label;
4558        if (entry.value) { entry.value = acNormalizeValue(entry.value); }
4559        if (isOfferEntry && (!entry.value || entry.value.type !== 'offer')) { return null; }
4560        return entry;
4561    }
4562
4563    function acHistoryBuildRecord(label, value) {
4564        const text = String(label || '').trim();
4565        return acHistoryNormalizeRecord({
4566            label: text,
4567            typeahead: text,
4568            suggestion: text,
4569            value: value || {}
4570        });
4571    }
4572
4573    // Lightweight history manager (localStorage-backed)
4574    const HISTORY = {
4575        KEY_SEARCH: 'search-history',
4576        KEY_OFFER: 'offer-history',
4577        KEY_AUTOCOMPLETE: 'ac-history',
4578        KEY_AUTOCOMPLETE_A: 'ac-history-a',
4579        get(key) {
4580            try {
4581                const raw = window.localStorage.getItem(key);
4582                if (!raw) return [];
4583                const arr = JSON.parse(raw);
4584                return Array.isArray(arr) ? arr : [];
4585            } catch (_e) { return []; }
4586        },
4587        put(key, item, limit = 9) {
4588            try {
4589                const arr = this.get(key);
4590                const entry = acHistoryNormalizeRecord(item);
4591                if (!entry || !entry.value) { return; }
4592                entry.ts = Date.now();
4593                const sig = acValueSig(entry.value ? entry.value : entry);
4594                const filtered = arr.filter(el => {
4595                    try { return acValueSig(el && el.value ? el.value : el) !== sig; } catch(_e){ return true; }
4596                });
4597                filtered.push(entry);
4598                const out = filtered
4599                    .slice()
4600                    .sort((a, b) => acHistoryEffectiveTs(b, 0) - acHistoryEffectiveTs(a, 0))
4601                    .slice(0, limit);
4602                window.localStorage.setItem(key, JSON.stringify(out));
4603            } catch(_e) {}
4604        },
4605        /** Newest-first across address + generic autocomplete keys (deduped by value signature). */
4606        recentAutocompleteItems(limit = 9) {
4607            const combined = this.get(this.KEY_AUTOCOMPLETE_A).concat(this.get(this.KEY_AUTOCOMPLETE));
4608            if (!combined.length) { return []; }
4609            const stamped = combined.map((it, i) => {
4610                const copy = acHistoryNormalizeRecord(it);
4611                if (!copy || !copy.value) { return null; }
4612                if (!(typeof copy.ts === 'number' && copy.ts > 0)) { copy.ts = i; }
4613                return copy;
4614            }
4614).filter(Boolean);
4615            stamped.sort((a, b) => acHistoryEffectiveTs(b, 0) - acHistoryEffectiveTs(a, 0));
4616            const uniq = [];
4617            const seen = new Set();
4618            for (let i = 0; i < stamped.length && uniq.length < limit; i++) {
4619                const it = stamped[i];
4620                const sig = acValueSig(it.value ? it.value : it);
4621                if (seen.has(sig)) { continue; }
4622                seen.add(sig);
4623                uniq.push(it);
4624            }
4625            return uniq;
4626        }
4627    };
4628
4629    try {
4630        window.__mirelaAcHistory = HISTORY;
4631        window.__mirelaAcHistoryBuildRecord = acHistoryBuildRecord;
4632    } catch (_eExpose) {}
4633
4634    class SearchAutocomplete {
4635        constructor(options = {}) {
4636            this.options = { ...CONFIG, ...options };
4637            this.searchTimeout = null;
4638            this.$inputs = null; // all matched inputs
4639            this.$input = null;  // active input
4640            this.$suggestions = null;
4641            this.$form = null;
4642            this.isInitialized = false;
4643            this.hasConfirmedSelection = false;
4644            this._autoSelecting = false;
4645            // Home-only: when user explicitly selects a suggestion, auto-trigger search ("Търси тук")
4646            this._homeAutoSearchPending = false;
4647            // Flag to prevent multiple search triggers from scheduled timeouts
4648            this._searchTriggerScheduled = false;
4649            // Home page has its own selection logic in `public/js/pages/home.js`.
4650            // On home we must ONLY provide suggestions rendering (typing -> /address -> dropdown),
4651            // and NOT attach selection / outside-click / submit handlers here.
4652            this._homeSuggestionsOnly = false;
4653            // Suppress autocomplete re-trigger right after a selection (ms timestamp)
4654            this.suppressUntilTs = 0;
4655            // Last real user keystroke on the location field (gates outside-click /address fallback)
4656            this._lastInputEventTs = 0;
4657            // Ignore stale /address responses when a newer fetch started or user confirmed a suggestion
4658            this._addressRequestGen = 0;
4659            this.init();
4660        }
4661
4662        cancelPendingAddressSuggestions() {
4663            try { clearTimeout(this.searchTimeout); } catch (_e) {}
4664            this.searchTimeout = null;
4665            this._addressRequestGen = (this._addressRequestGen || 0) + 1;
4666            this.suppressUntilTs = Date.now() + 800;
4667            try {
4668                if (this.$suggestions && this.$suggestions.length) {
4669                    this._collapseSuggestions(this.$suggestions);
4670                }
4671            } catch (_e) {}
4672            try {
4673                const $m = $('#mobileLocationModal .js-search-suggestions');
4674                if ($m.length) { this._collapseSuggestions($m); }
4675            } catch (_e) {}
4676        }
4677
4678        init() {
4679            // Detect homepage early so we can run in "suggestions-only" mode.
4680            try {
4681                this._homeSuggestionsOnly =
4682                    isHomePageShell();
4683            } catch (_e) { this._homeSuggestionsOnly = false; }
4684
4685            // Prefer the mobile modal input on small screens so suggestions appear in the modal
4686            const isMobile = isMobileLocationViewport();
4687            if (isMobile) {
4688                const $modalInput = $('#mobileLocationModal ' + this.options.selectors.input);
4689                if ($modalInput.length) {
4690                    this.$inputs = $modalInput;
4691                }
4692            }
4693            // Fallback to all search inputs if mobile-specific one is not found
4694            if (!this.$inputs || !this.$inputs.length) {
4695                this.$inputs = $(this.options.selectors.input);
4696            }
4697
4698            // Initial active input context
4699            this.$input = this.$inputs.first();
4700            this._setActiveContext(this.$input);
4701
4702            if (this.$input.length === 0 || this.$suggestions.length === 0) {
4703                // Няма поле за търсене на страницата
4704                return;
4705            }
4706
4707            // When the search bar is pre-filled (server-rendered, URL, saved context), treat the
4708            // value as "confirmed" so outside clicks (e.g. pagination) don't trigger the
4709            // auto-select logic which would re-open the suggestions dropdown.
4710            let initialSearchVal = '';
4711            try {
4712                initialSearchVal = (this.$input.val() || '').toString().trim();
4713                if (initialSearchVal.length) { this.hasConfirmedSelection = true; }
4714            } catch (_e) {}
4715            // Form already has structured location ids (SSR/URL/saved context) while the modal input may be empty.
4716            // Home/map may seed `city_id` alone for bounds — that must not count as a user-confirmed search label.
4717            try {
4718                if (this.$form && this.$form.length) {
4719                    const cid = (this.$form.find('[name="city_id"]').val() || '').toString().trim();
4720                    const did = (this.$form.find('[name="district_id"]').val() || '').toString().trim();
4721                    const dgid = (this.$form.find('[name="district_group_id"]').val() || '').toString().trim();
4722                    const sid = (this.$form.find('[name="street_id"]').val() || '').toString().trim();
4723                    if (did || dgid || sid || (cid && initialSearchVal.length)) { this.hasConfirmedSelection = true; }
4724                }
4725            } catch (_e) {}
4726
4727            this.bindEvents();
4728            this.isInitialized = true;
4729        }
4730
4731        _setActiveContext($inp) {
4732            try {
4733                if (!$inp || !$inp.length) return;
4734                this.$input = $inp;
4735                this.$suggestions = $inp.closest('.group-ip').find(this.options.selectors.suggestions).first();
4736                if (!this.$suggestions.length && $inp.closest('#mobileLocationModal').length) {
4737                    this.$suggestions = $inp.closest('#mobileLocationModal').find(this.options.selectors.suggestions).first();
4738                }
4739                if (!this.$suggestions.length) {
4740                    this.$suggestions = $(this.options.selectors.suggestions).first();
4741                }
4742                var $closestForm = $inp.closest('form');
4743                if ($closestForm && $closestForm.length) {
4744                    this.$form = $closestForm;
4745                } else {
4746                    // #mobileLocationModal: location-dropdown lives outside .js-search-form — closest('form') is empty
4747                    // and setOrCreateHidden would skip; bind hiddens to the real bar form (desktop + preview).
4748                    this.$form = $('.js-search-form').first();
4749                    if (!this.$form.length) {
4750                        this.$form = $('.js-mobile-search-form').first();
4751                    }
4752                }
4753            } catch (_e) {}
4754        }
4755
4756        _setActiveContextFromSuggestionEl(itemEl) {
4757            try {
4758                const $item = $(itemEl);
4759                const $sugg = $item.closest(this.options.selectors.suggestions);
4760                if ($sugg && $sugg.length) {
4761                    const $inp = $sugg.closest('.group-ip').find(this.options.selectors.input).first();
4762                    if ($inp && $inp.length) {
4763                        this._setActiveContext($inp);
4764                        return;
4765                    }
4766                }
4767            } catch (_e) {}
4768        }
4769
4770        _suggestionsHistoryClass() {
4771            return String(this.options.suggestionsHistoryClass || 'suggestions-from-history');
4772        }
4773
4774        _activeInputEl() {
4775            return this.$input && this.$input.length ? this.$input.get(0) : null;
4776        }
4777
4778        _preferredReopenTarget(inputEl) {
4779            try {
4780                const api = window.MirelaSearch && window.MirelaSearch.searchInputReopenTargetPersist;
4781                if (api && typeof api.getPersistedReopenTarget === 'function') {
4782                    return api.getPersistedReopenTarget();
4783                }
4784                if (typeof window.__mirelaGetMobileLocationModalReopenTarget === 'function') {
4785                    return window.__mirelaGetMobileLocationModalReopenTarget();
4786                }
4787            } catch (_e) {}
4788            return 'auto';
4789        }
4790
4791        _rememberSelectionSource(source, inputEl, meta) {
4792            try {
4793                const api = window.MirelaSearch && window.MirelaSearch.searchInputReopenTargetPersist;
4794                if (api && typeof api.setFromSource === 'function') {
4795                    api.setFromSource(source, meta || {});
4796                    return;
4797                }
4798                if (typeof window.__mirelaSetMobileLocationModalReopenTarget === 'function') {
4799                    if (source === 'suggestion') window.__mirelaSetMobileLocationModalReopenTarget('history');
4800                    else if (source === 'location-menu' || source === 'menu') {
4801                        window.__mirelaSetMobileLocationModalReopenTarget('location-menu');
4802                    }
4803                }
4804            } catch (_e) {}
4805        }
4806
4807        _resetPreferredReopenTarget(inputEl) {
4808            try {
4809                const api = window.MirelaSearch && window.MirelaSearch.searchInputReopenTargetPersist;
4810                if (api && typeof api.setFromSource === 'function') {
4811                    api.setFromSource('clear');
4812                } else if (typeof window.__mirelaSetMobileLocationModalReopenTarget === 'function') {
4813                    window.__mirelaSetMobileLocationModalReopenTarget('auto');
4814                }
4815            } catch (_e) {}
4816        }
4817
4818        _persistPreferredReopenTargetForCurrentState(inputEl) {
4819            try {
4820                const api = window.MirelaSearch && window.MirelaSearch.searchInputReopenTargetPersist;
4821                if (api && typeof api.persistBeforeNavigation === 'function') {
4822                    const form = this.$form && this.$form.length ? this.$form[0] : null;
4823                    const activeInput = inputEl || this._activeInputEl();
4824                    return api.persistBeforeNavigation(form, activeInput);
4825                }
4826            } catch (_eSharedPersist) {}
4827            return 'auto';
4828        }
4829
4830        _setHistoryEligible(eligible) {
4831            const inputEl = this._activeInputEl();
4832            if (!inputEl || !inputEl.dataset) return;
4833
4834            const next = eligible ? '1' : '0';
4835            inputEl.dataset.historyEligible = next;
4836
4837            const dropdown = inputEl.closest ? inputEl.closest('.location-dropdown') : null;
4838            if (dropdown && dropdown.dataset) {
4839                dropdown.dataset.historyEligible = next;
4840            }
4841        }
4842
4843        _isHistoryEligibleInput() {
4844            const inputEl = this._activeInputEl();
4845            return !!(inputEl && inputEl.dataset && inputEl.dataset.historyEligible === '1');
4846        }
4847
4848        /** Mobile fullscreen location modal: dedicated input (not the bar clone). */
4849        _isMobileModalLocationInput(inputEl) {
4850            try {
4851                return !!(inputEl && inputEl.closest && inputEl.closest('#mobileLocationModal'));
4852            } catch (_e) {
4853                return false;
4854            }
4855        }
4856
4857        _blurMobileModalInputAfterSuggestionPick() {
4858            try {
4859                if (!this._isMobileModalLocationInput(this._activeInputEl())) return;
4860                setTimeout(() => {
4861                    try {
4862                        if (typeof window.__mirelaBlurMobileModalLocationInput === 'function') {
4863                            window.__mirelaBlurMobileModalLocationInput();
4864                            return;
4865                        }
4866                        const el = this.$input && this.$input.length ? this.$input.get(0) : null;
4867                        if (el && typeof el.blur === 'function') el.blur();
4868                    } catch (_eBl) {}
4869                }, 0);
4870            } catch (_e) {}
4871        }
4872
4873        _hasVisibleInputValue(inputEl) {
4874            return !!String(inputEl && inputEl.value ? inputEl.value : '').trim();
4875        }
4876
4877        _readStructuredLocationSelection() {
4878            const $form = this.$form && this.$form.length ? this.$form : this.$input.closest('form');
4879            const read = (name) => {
4880                if (!$form || !$form.length) return '';
4881                const raw = $form.find(`[name="${name}"]`).first().val();
4882                return raw == null ? '' : String(raw).trim();
4883            };
4884
4885            return {
4886                cityId: read('city_id'),
4887                districtId: read('district_id'),
4888                districtGroupId: read('district_group_id'),
4889                streetId: read('street_id')
4890            };
4891        }
4892
4893        _hasStructuredLocationSelection() {
4894            const selection = this._readStructuredLocationSelection();
4895            return !!(
4896                selection.cityId ||
4897                selection.districtId ||
4898                selection.districtGroupId ||
4899                selection.streetId
4900            );
4901        }
4902
4903        /** True when only `city_id` is set and the visible search f
4903ield is empty (e.g. home map bootstrap). */
4904        _isCityIdOnlyWithoutVisibleSearchLabel(inputEl) {
4905            if (this._hasVisibleInputValue(inputEl)) {
4906                return false;
4907            }
4908            const selection = this._readStructuredLocationSelection();
4909            return !!(
4910                selection.cityId &&
4911                !selection.districtId &&
4912                !selection.districtGroupId &&
4913                !selection.streetId
4914            );
4915        }
4916
4917        maybeOpenLocationDropdownOnInputClick(inputEl) {
4918            const dropdown = inputEl && inputEl.closest ? inputEl.closest('.location-dropdown') : null;
4919            if (!dropdown || typeof dropdown.__openSelectionPanel !== 'function') {
4920                return false;
4921            }
4922
4923            // Empty field: dd-location opens only via "Меню" (.dd-toggle), never from input click.
4924            if (!this._hasVisibleInputValue(inputEl)) {
4925                return false;
4926            }
4927
4928            if (this._preferredReopenTarget(inputEl) === 'history') {
4929                return false;
4930            }
4931
4932            const menuEl = dropdown.querySelector ? dropdown.querySelector('.location-dd-menu') : null;
4933            const menuAlreadyOpen = !!(
4934                menuEl &&
4935                typeof window !== 'undefined' &&
4936                window.getComputedStyle &&
4937                window.getComputedStyle(menuEl).display !== 'none'
4938            );
4939            if (menuAlreadyOpen) {
4940                // location-dropdown.js is already switching away from menu mode on this click.
4941                // Re-opening the panel here would keep the menu stuck over recent-history suggestions.
4942                return false;
4943            }
4944
4945            if (this._isCityIdOnlyWithoutVisibleSearchLabel(inputEl)) {
4946                return false;
4947            }
4948
4949            const hasStructuredLocation = this._hasStructuredLocationSelection();
4950            const hasConfirmedValue = String(inputEl.value || '').trim() !== '' && this.hasConfirmedSelection;
4951            const hasVisibleValue = this._hasVisibleInputValue(inputEl);
4952
4953            if (!hasStructuredLocation && !hasConfirmedValue && hasVisibleValue) {
4954                return false;
4955            }
4956
4957            this._setHistoryEligible(false);
4958            this._collapseSuggestions(this.$suggestions);
4959            dropdown.__openSelectionPanel();
4960            return true;
4961        }
4962
4963        _resolveInputClickTarget(inputEl, options = {}) {
4964            const preferredTarget = this._preferredReopenTarget(inputEl);
4965            const hasVisibleValue = this._hasVisibleInputValue(inputEl);
4966
4967            // Empty field always opens recent history on input click (never dd-location).
4968            if (!hasVisibleValue) {
4969                return 'history';
4970            }
4971
4972            if (preferredTarget === 'location-menu') return 'location-menu';
4973            if (preferredTarget === 'history' || options.allowEmptyHistory) return 'history';
4974            return 'none';
4975        }
4976
4977        /** Hide and empty suggestions; clears history-panel marker used for click-toggle. */
4978        _collapseSuggestions($el) {
4979            try {
4980                const $t = ($el && $el.length) ? $el : this.$suggestions;
4981                if ($t && $t.length) {
4982                    $t.removeClass(this._suggestionsHistoryClass()).hide().empty();
4983                }
4984            } catch (_e) {}
4985        }
4986
4987        /** History is available only after explicit X-clear on an empty field. */
4988        toggleHistorySuggestionsOnInputClick(forceHistory) {
4989            try {
4990                const $sug = this.$suggestions;
4991                if (!$sug || !$sug.length) return;
4992                const shouldForceHistory = !!forceHistory;
4993                const inputValue = String(this.$input && this.$input.length ? this.$input.val() || '' : '').trim();
4994                if (!shouldForceHistory && (inputValue !== '' || !this._isHistoryEligibleInput())) {
4995                    this._collapseSuggestions($sug);
4996                    return;
4997                }
4998                if ($sug.is(':visible') && $sug.hasClass(this._suggestionsHistoryClass())) {
4999                    // Focus then click on the same field would otherwise toggle history off immediately (mobile modal).
5000                    if (shouldForceHistory && this._isMobileModalLocationInput(this._activeInputEl())) {
5001                        return;
5002                    }
5003                    this._collapseSuggestions($sug);
5004                    return;
5005                }
5006                this.maybeShowRecentHistory();
5007            } catch (_e) {}
5008        }
5009
5010        bindEvents() {
5011            // Existing selected location opens the dropdown panel; history opens only after X-clear.
5012            this.$inputs.on('focus', (ev) => {
5013                this._setActiveContext($(ev.currentTarget));
5014            });
5015            this.$inputs.on('click', (ev) => {
5016                this._setActiveContext($(ev.currentTarget));
5017                try {
5018                    const el = ev.currentTarget;
5019                    if (this._isMobileModalLocationInput(el)) {
5020                        const q = String(($(el).val() || '')).trim();
5021                        if (q === '') {
5022                            // Empty modal field: focusin in mobile-location-modal.js owns first history show.
5023                            return;
5024                        }
5025                        if (typeof window.__mobileLocationSetMode === 'function') {
5026                            window.__mobileLocationSetMode('typing');
5027                        }
5028                        this.toggleHistorySuggestionsOnInputClick(true);
5029                        return;
5030                    }
5031                } catch (_e) {}
5032                try {
5033                    if (this._homeSuggestionsOnly) {
5034                        const el = ev.currentTarget;
5035                        if (!this._isMobileModalLocationInput(el)) {
5036                            const qDesktop = String(($(el).val() || '')).trim();
5037                            if (!qDesktop) {
5038                                this.toggleHistorySuggestionsOnInputClick(true);
5039                                return;
5040                            }
5041                        }
5042                    }
5043                } catch (_eHomeDesk) {}
5044                const clickTarget = this._resolveInputClickTarget(ev.currentTarget);
5045
5046                if (clickTarget === 'history') {
5047                    this.toggleHistorySuggestionsOnInputClick(true);
5048                    return;
5049                }
5050
5051                if (clickTarget === 'location-menu' && this.maybeOpenLocationDropdownOnInputClick(ev.currentTarget)) {
5052                    try { ev.stopPropagation(); } catch (_eStop) {}
5053                    return;
5054                }
5055                this.toggleHistorySuggestionsOnInputClick(false);
5056            });
5057            // Handle input keyup
5058            this.$inputs.on('keyup input', (e) => {
5059                this._setActiveContext($(e.currentTarget));
5060                this.handleInputKeyup(e);
5061            });
5062
5063            // Home page: suggestions-only mode.
5064            // Do NOT bind any delegated click/blur/outside/submit handlers here to avoid double-handling
5065            // with `public/js/pages/home.js`.
5066            if (this._homeSuggestionsOnly) {
5067                // Home: история само премахване по ред (X). Clear-all към историята е премахнат по продуктова заявка.
5068                // Home.js intentionally ignores clicks on `.suggestion-item-remove`, so we can safely handle them here.
5069                $(document).on('click.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item-remove', (e) => {
5070                    try {
5071                        e.preventDefault();
5072                        e.stopPropagation();
5073                        if (e.stopImmediatePropagation) { e.stopImmediatePropagation(); }
5074                    } catch(_e) {}
5075                    this._removeHistorySuggestionItem(e.currentTarget);
5076                });
5077
5078                $(document).on('pointerdown.searchAutocomplete mousedown.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item-remove', (e) => {
5079                    try { e.preventDefault(); } catch(_e) {}
5080                    try { e.stopPropagation(); } catch(_e) {}
5081                    if (e.stopImmediatePropagation) { try { e.stopImmediatePropagation(); } catch(_e) {} }
5082                });
5083
5084                $(document).on('click.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item', (e) => {
5085                    if ($(e.target).closest('.suggestion-item-remove').length) return;
5086                    try {
5087                        const $pick = $(e.currentTarget);
5088                        let val = $pick.data('value');
5089                        if (typeof val === 'string') { try { val = JSON.parse(val); } catch(_e) {} }
5090                        const label = ($pick.find('.suggestion-item-text').text() || $pick.text() || '').trim();
5091                        const sc = window.MirelaSearch && window.MirelaSearch.searchSubmitController;
5092                        if (sc && typeof sc.tryNavigateToOfferFromHistoryPick === 'function' &&
5093                            sc.tryNavigateToOfferFromHistoryPick(val, label)) {
5094                            try { e.preventDefault(); } catch(_e) {}
5095                            try { e.stopPropagation(); } catch(_e) {}
5096                            if (e.stopImmediatePropagation) { try { e.stopImmediatePropagation(); } catch(_e) {} }
5097                            return;
5098                        }
5099                    } catch (_eHomeOffer) {}
5100                });
5101
5102                return;
5103            }
5104
5105            // Prevent mousedown on suggestion items from blurring the input.
5106            // Without this, the blur handler hides suggestions before the click event fires,
5107            // making it impossible to select items (especially history items on empty input).
5108            this._skipNextClick = false;
5109            $(document).on('pointerdown.searchAutocomplete mousedown.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item', (e) => {
5110                // only primary button
5111                try { if (typeof e.button !== 'undefined' && e.button !== 0) { return; } } catch(_e) {}
5112                // Prevent focus loss so blur doesn't hide suggestions before click fires
5113                try { e.preventDefault(); } catch(_e) {}
5114                this._setActiveContextFromSuggestionEl(e.currentTarget);
5115
5116                // On offer preview pages, handle selection immediately on mousedown
5117                // (click may be eaten by other handlers on those pages)
5118                try {
5119                    if (isActualOfferPreviewPage()) {
5120                        try { e.stopPropagation(); } catch(_e) {}
5121                        this._skipNextClick = true;
5122                        this.suppressUntilTs = Date.now() + 800;
5123                        try { clearTimeout(this.searchTimeout); } catch(_e) {}
5124                        try { this.handleSuggestionClick({ currentTarget: e.currentTarget, stopPropagation: () => {} }); } catch(_e) {}
5125                    }
5126                } catch (_e) {}
5127            });
5128
5129            // Handle suggestion click
5130            $(document).on('click.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item', (e) => {
5131                // Mobile location modal has a dedicated handler in location-dropdown.js.
5132                // Skip generic autocomplete handling here to avoid state races.
5133                try {
5134                    if ($(e.currentTarget).closest('#mobileLocationModal').length) { return; }
5135                } catch(_e) {}
5136                try {
5137                    const $pickOffer = $(e.currentTarget);
5138                    let rawOffer = $pickOffer.data('value');
5139                    if (typeof rawOffer === 'string') { try { rawOffer = JSON.parse(rawOffer); } catch(_e) {} }
5140                    const pickLabel = ($pickOffer.find('.suggestion-item-text').text() || $pickOffer.text() || '').trim();
5141                    const scOfferAc = window.MirelaSearch && window.MirelaSearch.searchSubmitController;
5142                    if (scOfferAc && typeof scOfferAc.tryNavigateToOfferFromHistoryPick === 'function' &&
5143                        scOfferAc.tryNavigateToOfferFromHistoryPick(rawOffer, pickLabel)) {
5144                        try { e.preventDefault(); } catch(_e) {}
5145                        try { e.stopPropagation(); } catch(_e) {}
5146                        if (e.stopImmediatePropagation) { try { e.stopImmediatePropagation(); } catch(_e) {} }
5147                        return;
5148                    }
5149                } catch (_eOfferPick) {}
5150                // If we already handled this selection on pointerdown/mousedown, skip duplicate click
5151                if (this._skipNextClick) {
5152                    this._skipNextClick = false;
5153                    return;
5154                }
5155                this._setActiveContextFromSuggestionEl(e.currentTarget);
5156                // Suppress immediate re-open from synthetic input/change events
5157                this.suppressUntilTs = Date.now() + 800;
5158                try { clearTimeout(this.searchTimeout); } catch(_e) {}
5159                // Fill input + hidden ids here. On list/home/offer preview we stop propagation so
5160                // page handlers don't double-fire; submit shells auto-submit after handleSuggestionClick.
5161                try { e.preventDefault(); } catch(_e) {}
5162                try {
5163                    e.stopPropagation();
5164                    if (e.stopImmediatePropagation) { e.stopImmediatePropagation(); }
5165                } catch (_e) {}
5166                try { this._collapseSuggestions(this.$suggestions); } catch(_e) {}
5167                this.handleSuggestionClick(e);
5168            });
5169
5170            // Remove individual suggestion item via X button
5171            $(document).on('click.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item-remove', (e) => {
5172                e.preventDefault();
5173                e.stopPropagation();
5174                if (e.stopImmediatePropagation) { e.stopImmediatePropagation(); }
5175                this._removeHistorySuggestionItem(e.currentTarget);
5176            });
5177
5178            // Prevent mousedown on X from blurring input
5179            $(document).on('pointerdown.searchAutocomplete mousedown.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item-remove', (e) => {
5180                try { e.preventDefault(); } catch(_e) {}
5181                try { e.stopPropagation(); } catch(_e) {}
5182                if (e.stopImmediatePropagation) { try { e.stopImmediatePropagation(); } catch(_e) {} }
5183            });
5184
5185            // Hide suggestions when clicking outside (or auto-select first if user typed but didn't choose)
5186            $(document).on('click', (e) => {
5187                this.handleOutsideClick(e);
5188            });
5189
5190            // Also handle blur on the input similarly
5191            this.$inputs.on('blur', (ev) => {
5192                this._setActiveContext($(ev.currentTarget));
5193                try {
5194                    const kbAt = window.__mirelaLocationSuggestKeyboardAt || 0;
5195                    if (kbAt && (Date.now() - kbAt) < 250) {
5196                        return;
5197                    }
5198                } catch (_eKbBlur) {}
5199                this.handleOutsideClick({ target: document.body });
5200            });
5201
5202            // Clear hidden location filters when input is cleared
5203            this.$inputs.on('input', (e) => {
5204                this._setActiveContext($(e.currentTarget));
5205                this.handleInputChange(e);
5206            });
5207
5208            // Handle form submission - bind to both the input's form and .js-search-form (may be different on mobile)
5209            const $inputForm = this.$input.closest('form');
5210            const $jsSearchForm = $('.js-search-form');
5211            
5212            if ($inputForm.length) {
5213                $inputForm.on('submit', (e) => {
5214                    this.handleFormSubmit(e);
5215                });
5216                $inputForm.on('click', '.btn-search', (e) => {
5217                    this.handleSearchButtonClick(e);
5218                });
5219            }
5220            
5221            // Also bind to .js-search-form if different from input's form (mobile modal case)
5222            if ($jsSearchForm.length && (!$inputForm.length || $jsSearchForm[0] !== $inputForm[0])) {
5223                $jsSearchForm.on('submit', (e) => {
5224                    this.handleFormSubmit(e);
5225                });
5226                $jsSearchForm.on('click', '.btn-search', (e) => {
5227                    this.handleSearchButtonClick(e);
5228                });
5229            }
5230
5231        }
5232
5233        handleInputKeyup(e) {
5234            if (isSuggestionNavigationKey(e)) {
5235                return;
5236            }
5237            // Mobile location modal mode switching:
5238            // typing in the field should force "typing" mode (hide lower menu),
5239            // clearing the field returns to default mode.
5240            try {
5241                const inMobileModal = !!(this.$input && this.$input.length && this.$input.closest('#mobileLocationModal').length);
5242                if (inMobileModal) {
5243                    const q = String((this.$input.val() || '')).trim();
5244                    const modeBefore = (typeof window.__mobileLocationGetMode === 'function')
5245                        ? window.__mobileLocationGetMode()
5246                        : 'default';
5247                    const canSwitchFromMenu =
5248                        (typeof window.__mobileLocationHasRecentTyping === 'function')
5249                            ? window.__mobileLocationHasRecentTyping()
5250                            : true;
5251                    if (typeof window.__mobileLocationSetMode === 'function') {
5252                        if (q === '') {
5253                            let browseInitial = false;
5254                            try {
5255                                const bm = document.getElementById('mobileLocationModal');
5256                                browseInitial = !!(bm && bm.getAttribute && bm.getAttribute('data-loc-browse-initial') === '1');
5257                            } catch (_eBr) {
5258                                browseInitial = false;
5259                            }
5260                            window.__mobileLocationSetMode(browseInitial ? 'menu' : 'default');
5261                        } else if (modeBefore !== 'menu' || canSwitchFromMenu) {
5262                            window.__mobileLocationSetMode('typing');
5263                        }
5264                    }
5265                    const modeNow = (typeof window.__mobileLocationGetMode === 'function')
5266                        ? window.__mobileLocationGetMode()
5267                        : 'default';
5268                    if (modeNow === 'menu') {
5269                        try { this._collapseSuggestions(this.$suggestions); } catch(_e) {}
5270                        return;
5271                    }
5272                }
5273            } catch(_e) {}
5274
5275            // Home suggestions-only: ignore programmatic input/change events dispatched by home.js after selection.
5276            // Otherwise the dropdown re-opens right after the map moves (user experience: "I selected Varna and it opens again").
5277            if (this._homeSuggestionsOnly) {
5278                let trusted = false;
5279                try { trusted = !!(e && (e.isTrusted === true || (e.originalEvent && e.originalEvent.isTrusted === true))); } catch(_e) { trusted = false; }
5280                if (!trusted) {
5281                    try { this.$suggestions && this._collapseSuggestions(this.$suggestions); } catch(_e) {}
5282                    return;
5283                }
5284            }
5285            // If we are within suppression window after a selection, skip re-opening
5286            if (this.suppressUntilTs && Date.now() < this.suppressUntilTs) {
5287                try { clearTimeout(this.searchTimeout); } catch(_e) {}
5288                try { this._collapseSuggestions(this.$suggestions); } catch(_e) {}
5289                return;
5290            } else if (this.suppressUntilTs) {
5291                this.suppressUntilTs = 0;
5292            }
5293            const searchTerm = this.$input.val().trim();
5294            if (searchTerm !== '') {
5295                this._setHistoryEligible(false);
5296            }
5297            this.hasConfirmedSelection = false;
5298            clearTimeout(this.searchTimeout);
5299            if (/^\d+$/.test(searchTerm)) {
5300                this._collapseSuggestions(this.$suggestions);
5301                return;
5302            }
5303            if (searchTerm.length < this.options.minSearchLength) {
5304                // For short terms – do NOT hit AJAX. History opens only via input click (toggle).
5305                this._collapseSuggestions(this.$suggestions);
5306                return;
5307            }
5308            this.searchTimeout = setTimeout(() => {
5309                this.fetchSuggestions(searchTerm);
5310            }, this.options.searchDelay);
5311        }
5312
5313        fetchSuggestions(searchTerm) {
5314            this._addressRequestGen = (this._addressRequestGen || 0) + 1;
5315            const reqGen = this._addressRequestGen;
5316            this._lastSearchTerm = String(searchTerm || '').trim();
5317            $.ajax({
5318                url: this.options.ajaxUrl,
5319                dataType: 'json',
5320                data: { term: searchTerm },
5321                success: (data) => {
5322                    if (reqGen !== this._addressRequestGen) return;
5323                    this.displaySuggestions(data);
5324                },
5325                error: (xhr, status, error) => {
5326                    if (reqGen !== this._addressRequestGen) return;
5327                    this._collapseSuggestions(this.$suggestions);
5328                }
5329            });
5330        }
5331
5332        displaySuggestions(data) {
5333            // Suppress only blocks AJAX-driven reopen right after a selection — not recent-history UI.
5334            // Otherwise the first focus/click after X-clear can hit this window and show an empty dropdown (E2E + UX).
5335            const isHistory = !!(data && data[0] && data[0].value && data[0].value.__fromHistory);
5336            if (!isHistory && this.suppressUntilTs && Date.now() < this.suppressUntilTs) {
5337                try { this._collapseSuggestions(this.$suggestions); } catch (_e) {}
5338                return;
5339            }
5340            if (data && data.length > 0) {
5341                if (!isHistory) {
5342                    const term = String(this._lastSearchTerm || '').trim().toLowerCase();
5343                    const scoreSuggestion = (item) => {
5344                        const label = String((item && item.label) || '').trim();
5345                        const low = label.toLowerCase();
5346                        const isRegion = /^\s*(област|region|обл\.|регион)/i.test(label);
5347                        const isCity = /^\s*(град|city of|g\.|gorod|stadt)/i.test(label) || !isRegion;
5348                        const normalized = low
5349                            .replace(/^\s*(град|city of|g\.|gorod|stadt|област|region|обл\.|регион)\s+/i, '')
5350                            .trim();
5351                        let score = 0;
5352                        if (term && normalized === term) score += 100;
5353                        else if (term && low.includes(term)) score += 40;
5354                        if (isCity) score += 20;
5355                        if (isRegion) score -= 20;
5356                        return score;
5357                    };
5358                    data = data.slice().sort((a, b) => scoreSuggestion(b) - scoreSuggestion(a));
5359                }
5360                let html = '';
5361                data.forEach((item) => {
5362                    const val = JSON.stringify(item.value || {}).replace(/'/g, '&#39;');
5363                    if (isHistory) {
5364                        html += `<div class="suggestion-item" data-value='${val}'>`
5365                             +    `<span class="suggestion-item-text">${item.label || ''}</span>`
5366                             +    `<button type="button" class="suggestion-item-remove pt-clear-x" title="Премахни" aria-label="Премахни от историята">${CONFIG.clearXSvg}</button>`
5367                             +  `</div>`;
5368                    } else {
5369                        html += `<div class="suggestion-item" data-value='${val}'>${item.label || ''}</div>`;
5370                    }
5371                });
5372                if (isHistory) {
5373                    this.$suggestions.addClass(this._suggestionsHistoryClass());
5374                } else {
5375                    this.$suggestions.removeClass(this._suggestionsHistoryClass());
5376                }
5377                this.$suggestions.html(html).show();
5378                try {
5379                    const eh = window.MirelaSearch && window.MirelaSearch.searchInputEnterHandler;
5380                    const inp = this.$input && this.$input.length ? this.$input.get(0) : null;
5381                    const panel = this.$suggestions && this.$suggestions.length ? this.$suggestions.get(0) : null;
5382                    if (eh && typeof eh.restoreKeyboardHighlight === 'function') {
5383                        eh.restoreKeyboardHighlight(inp, panel);
5384                    }
5385                } catch (_eKbRestore) {}
5386            } else {
5387                this._collapseSuggestions(this.$suggestions);
5388            }
5389
5390            // Mobile location modal: respect mode switching managed by location-dropdown.js
5391            // (typing/menu/default). Keep menu hidden while typing and hidden suggestions in menu mode.
5392            try {
5393                const inMobileModal = !!(this.$input && this.$input.length && this.$input.closest('#mobileLocationModal').length);
5394                if (inMobileModal) {
5395                    const mode = (typeof window.__mobileLocationGetMode === 'function')
5396                        ? window.__mobileLocationGetMode()
5397                        : 'default';
5398                    const hasQuery = String((this.$input.val() || '')).trim() !== '';
5399                    if (typeof window.__mobileLocationSetMode === 'function') {
5400                        if (mode === 'menu' && !isHistory) {
5401                            this._collapseSuggestions(this.$suggestions);
5402                        } else if (hasQuery || isHistory) {
5403                            // Recent-history (empty field) is a typing-shell view: hide dd-location menu.
5404                            window.__mobileLocationSetMode('typing');
5405                        } else {
5406                            window.__mobileLocationSetMode('default');
5407                        }
5408                    }
5409                }
5410            } catch(_e) {}
5411        }
5412
5413        handleSuggestionClick(e) {
5414            const $itemEarly = $(e.currentTarget);
5415            let valueEarly = $itemEarly.data('value');
5416            if (typeof valueEarly === 'string') {
5417                try { valueEarly = JSON.parse(valueEarly); } catch(_e) {}
5418            }
5419            const $txtEarly = $itemEarly.find('.suggestion-item-text');
5420            const textEarly = ($txtEarly.length ? $txtEarly.text() : $itemEarly.text() || '').trim();
5421            var scEarly = window.MirelaSearch && window.MirelaSearch.searchSubmitController;
5422            if (scEarly && typeof scEarly.tryNavigateToOfferFromHistoryPick === 'function' &&
5423                scEarly.tryNavigateToOfferFromHistoryPick(valueEarly, textEarly)) {
5424                return;
5425            }
5426            try {
5427                window.__mirelaLocationCommitNoMap = true;
5428                window.__mirelaHardStopStoredHomeMapRestore = true;
5429                window.__mirelaAppliedStoredHomeMapViewOnce = true;
5430                window.__mirelaBlockStoredHomeMapRestoreUntil = Date.now() + 15000;
5431                window.__mirelaRestoringUserMapView = false;
5432                window.__mirelaExplicitLocationFit = true;
5433                window.__forceHomeAutoFitOnce = true;
5434                window.__mirelaAutocompleteSelection = true;
5435                setTimeout(function () {
5436                    try { window.__mirelaExplicitLocationFit = false; } catch(_e1) {}
5437                    try { window.__mirelaAutocompleteSelection = false; } catch(_e2) {}
5438                }, 2500);
5439            } catch(_e) {}
5440            const $item = $(e.currentTarget);
5441            const showMobileBaseMenu = () => {
5442                try {
5443                    const inMobileModal = !!(this.$input && this.$input.length && this.$input.closest('#mobileLocationModal').length);
5444                    if (!inMobileModal) return;
5445                    const $menu = $('#mobileLocationModal .location-dropdown .location-dd-menu').first();
5446                    if ($menu.length) { $menu.show(); }
5447                } catch(_e) {}
5448            };
5449            // Selecting a location from autocomplete is a hard reset of map-bounds search intent:
5450            // the subsequent /map-browse-offers request must NOT include map coords, even if the
5451            // user had earlier panned/zoomed the map or "Търси като местиш" is checked. We clear
5452            // the stale URL ?map=/?map_manual= and all viewport-driven flags here — before the
5453            // user presses "Търси" — so every downstream code path (list.js submit handler,
5454            // triggerManualSearch, prepareListFormMapForRequest) sees a clean state.
5455            try {
5456                const _u = new URL(window.location.href);
5457                if (_u.searchParams.has('map') || _u.searchParams.has('map_manual')) {
5458                    _u.searchParams.delete('map');
5459                    _u.searchParams.delete('map_manual');
5460                    history.replaceState(null, '', _u.pathname + (_u.search || '') + (_u.hash || ''));
5461                }
5462            } catch(_eStripMapUrl) {}
5463            try { window.__listBoundsMode = false; } catch(_e) {}
5464            try { window.__landedWithMap = false; } catch(_e) {}
5465            try {
5466                if (window.MirelaSearch && typeof window.MirelaSearch.applyUserMovedMapList === 'function') {
5467                    window.MirelaSearch.applyUserMovedMapList(false);
5468                } else {
5469                    window.__mirelaUserMovedMap = false;
5470                }
5471            } catch(_e) {}
5472            try { window.__mapManualNext = false; } catch(_e) {}
5473            let value = $item.data('value');
5474            // Robustly parse JSON string payloads
5475            if (typeof value === 'string') {
5476                try { value = JSON.parse(value); } catch(_e) {}
5477            }
5478            // New explicit location beats offer-preview "user cleared" session state: otherwise label
5479            // rehydration wipes the bar on next offer navigation even though the user chose again on home.
5480            try {
5481                const addrPick = !!(value && (value.cities || value.districts || value.district_groups || value.street_id));
5482                if (addrPick) {
5483                    const locState = window.MirelaSearch && window.MirelaSearch.locationState;
5484                    if (locState && typeof locState.markSelected === 'function') {
5485                        locState.markSelected({ label: '', value: value });
5486                    }
5487                }
5488            } catch (_eClrPrev) {}
5489
5490            const $txt = $item.find('.suggestion-item-text');
5491            const text = $txt.length ? $txt.text() : $item.text();
5492            this.$input.val(text);
5493            try {
5494                const elPicked = this.$input.get(0);
5495                const FSscroll = window.MirelaLocationFormSync;
5496                if (FSscroll && typeof FSscroll.scrollMobileSearchInputToStart === 'function' && elPicked) {
5497                    FSscroll.scrollMobileSearchInputToStart(elPicked);
5498                }
5499                if (FSscroll && typeof FSscroll.syncCompactMobileInput === 'function') {
5500                    FSscroll.syncCompactMobileInput(text);
5501                }
5502            } catch (_eMobScroll) {}
5503            try {
5504                const locState2 = window.MirelaSearch && window.MirelaSearch.locationState;
5505                if (locState2 && typeof locState2.markSelected === 'function') {
5506                    locState2.markSelected({ label: text, value: value || {} });
5507                }
5508            } catch (_eLocState2) {}
5509            this._setHistoryEligible(false);
5510            const normCityId = (value && value.cities)
5511                ? String(Array.isArray(value.cities) ? (value.cities[0] || '') : value.cities).trim()
5512                : '';
5513            this._rememberSelectionSource('suggestion', null, { cityId: normCityId, label: text });
5514            // Mark selection BEFORE dispatching the input event so handleInputKeyup
5515            // (which runs synchronously from the dispatched event) won't schedule a
5516            // new fetchSuggestions that would re-open the dropdown after we hide it.
5517            this.hasConfirmedSelection = true;
5518            this.suppressUntilTs = Date.now() + 600;
5519            // Notify input listeners so dropdown toggle (X) appears
5520            try {
5521                const el = this.$input.get(0);
5522                if (el) {
5523                    el.dispatchEvent(new Event('input', { bubbles: true }));
5524                    // Do NOT dispatch 'change' here. On some pages it triggers map/search logic.
5525                    // 'input' is enough to update UI (clear button).
5526                }
5527            } catch(_e) {}
5528
5529            this._blurMobileModalInputAfterSuggestionPick();
5530
5531            // Save selection to history (Autocomplete Address history by default)
5532            try {
5533                const rec = { label: text, typeahead: text, suggestion: text, value: value || {} };
5534                const isAddress = !!(rec.value && (rec.value.cities || rec.value.districts || rec.value.district_groups || rec.value.street_id));
5535                const key = isAddress ? HISTORY.KEY_AUTOCOMPLETE_A : HISTORY.KEY_AUTOCOMPLETE;
5536                const limit = isAddress ? this.options.historyLimits.acA : this.options.historyLimits.ac;
5537                HISTORY.put(key, rec, limit);
5538            } catch(_e) {}
5539
5540            try {
5541                const isListPage = isListPageShell();
5542                const isPresetPath = /-offer\d+(?:\.html)?$/i.test(String(window.location.pathname || ''));
5543                const isAddressValue = !!(value && (value.cities || value.districts || value.district_groups || value.street_id));
5544                if (isListPage && isPresetPath && isAddressValue) {
5545                    window.__listPresetBreakoutNext = true;
5546                }
5547            } catch (_ePresetBreakout) {}
5548
5549            // Offer preview pages build the target list URL from explicit id filters (city_id/district_id/...),
5550            // and intentionally drop free-text `s` (SEO). Ensure selecting an autocomplete address writes ids.
5551            // Otherwise the user sees "Варна" in the box, but the search executes with no location.
5552            // Selecting a suggestion/history item must auto-submit just like the price/area ribbons:
5553            // commit the ids, then run the preview→list redirect via MirelaSearch.range.queueSubmit.
5554            // The mobile #mobileLocationModal flow has its own commit/OK path (location-dropdown.js), so skip it here.
5555            try {
5556                if (isActualOfferPreviewPage()) {
5557                    const isAddressValue = !!(value && (value.cities || value.districts || value.district_groups || value.street_id));
5558                    if (isAddressValue) {
5559                        const normCity = (value && value.cities)
5560                            ? (Array.isArray(value.cities) ? (value.cities[0] || '') : value.cities)
5561                            : '';
5562                        this._syncStructuredLocationHiddenFields(value, normCity || '');
5563                    }
5564                    try { this._collapseSuggestions(this.$suggestions); } catch(_e) { try { this._collapseSuggestions(this.$suggestions); } catch(_e2) {} }
5565                    showMobileBaseMenu();
5566                    const inMobileModalPreviewPick = !!(this.$input && this.$input.length && this.$input.closest('#mobileLocationModal').length);
5567                    if (isAddressValue && !inMobileModalPreviewPick) {
5568                        triggerPreviewSubmitAfterSuggestionPick(this.$form && this.$form[0] ? this.$form[0] : null);
5569                    }
5570                    return;
5571                }
5572            } catch (_e) {}
5573            // Static/external/demands shells: persist structured location ids when present, then auto-submit when enabled.
5574            try {
5575                if (isStaticLocationPickShell()) {
5576                    const isAddressValue = !!(value && (value.cities || value.districts || value.district_groups || value.street_id));
5577                    if (isAddressValue) {
5578                        const normalizedCityId = (value && value.cities)
5579                            ? (Array.isArray(value.cities) ? (value.cities[0] || '') : value.cities)
5580                            : '';
5581                        this._syncStructuredLocationHiddenFields(value, normalizedCityId || '');
5582                        this._syncCommittedLocationSearchLabel(text);
5583                        try { this._patchUiSessionAfterLocationPick(text, value, normalizedCityId || ''); } catch (_eUiExternal) {}
5584                    }
5585                    try { this._collapseSuggestions(this.$suggestions); } catch (_eCollapseExternal) {}
5586                    showMobileBaseMenu();
5587                    triggerAutoSubmitAfterSuggestionPick(this.$form && this.$form[0] ? this.$form[0] : null);
5588                    return;
5589                }
5590            } catch (_eExternal) {}
5591            // Non-AJAX list shells (e.g. all-properties): sync hidden filters; auto-submit when shell allows it.
5592            try {
5593                if (window.__disableAjaxList) {
5594                    const normCity = (value && value.cities) ? (Array.isArray(value.cities) ? (value.cities[0] || '') : value.cities) : '';
5595                    this._syncStructuredLocationHiddenFields(value, normCity || '');
5596                    try { this._collapseSuggestions(this.$suggestions); } catch(_e){}
5597                    showMobileBaseMenu();
5598                    triggerAutoSubmitAfterSuggestionPick(this.$form && this.$form[0] ? this.$form[0] : null);
5599                    return;
5600                }
5601            } catch(_e) {}
5602            // Do NOT write raw JSON into the hidden map input.
5603            // The map field expects a "neLng|neLat|swLng|swLat|zoom" string and
5604            // writing JSON causes parsing errors and flicker.
5605            this._collapseSuggestions(this.$suggestions);
5606            try {
5607                const inMobileModal = !!(this.$input && this.$input.length && this.$input.closest('#mobileLocationModal').length);
5608                const isListPage = (isListPageShell());
5609                const isMobile = isMobileLocationViewport();
5610                if (inMobileModal && isListPage && isMobile) {
5611                    if (typeof window.__mirelaMobileModalAfterTopFieldSuggestion === 'function') {
5612                        window.__mirelaMobileModalAfterTopFieldSuggestion();
5613                    } else {
5614                        try { if (typeof window.__mobileLocationSetMode === 'function') window.__mobileLocationSetMode('typing'); } catch (_e) {}
5615                        try {
5616                            const r = document.getElementById('mobileLocationModal');
5617                            if (r && r.removeAttribute) r.removeAttribute('data-loc-browse-initial');
5618                        } catch (_e) {}
5619                        try {
5620                            const dd = document.querySelector('#mobileLocationModal .location-dropdown');
5621                            const m = dd && dd.querySelector ? dd.querySelector('.location-dd-menu') : null;
5622                            if (m) m.style.display = 'none';
5623                            if (dd) dd.classList.add('open');
5624                        } catch (_e) {}
5625                    }
5626                } else {
5627                    showMobileBaseMenu();
5628                }
5629            } catch (_e) {
5630                try { showMobileBaseMenu(); } catch (_e2) {}
5631            }
5632
5633            // Mobile on list page: bypass map, set city_id and trigger list load directly
5634            try {
5635                const isListPage = (isListPageShell());
5636                const isMobile = isMobileLocationViewport();
5637                if (isListPage && isMobile) {
5638                    const $form = $('.js-search-form');
5639                    const legacyLocationCompat = window.MirelaLocationFormLegacyCompat || null;
5640                    const selectionApi = window.MirelaLocationSelection || null;
5641                    if (legacyLocationCompat && typeof legacyLocationCompat.applySelectionToJqueryForm === 'function') {
5642                        legacyLocationCompat.applySelectionToJqueryForm($form, value, {
5643                            searchLabel: text,
5644                            persistCityPreset: true,
5645                            ensureCityHidden: true,
5646                            removeCityInputs: false,
5647                            ensureLocationHidden: true,
5648                            clearMap: true,
5649                            forceCityOnce: true
5650                        });
5651                        this._syncCommittedLocationSearchLabel(text);
5652                    } else if (selectionApi && typeof selectionApi.writeLocationToForm === 'function') {
5653                        const formEl = $form && $form.get ? $form.get(0) : null;
5654                        const normalizedCityId = value && value.cities
5655                            ? (Array.isArray(value.cities) ? value.cities[0] : value.cities)
5656                            : '';
5657                        selectionApi.writeLocationToForm(formEl, {
5658                            cityId: normalizedCityId || '',
5659                            districtIds: value && value.districts
5660                                ? (Array.isArray(value.districts) ? value.districts : [value.districts])
5661                                : [],
5662                            groupIds: value && value.district_groups
5663                                ? (Array.isArray(value.district_groups) ? value.district_groups : [value.district_groups])
5664                                : [],
5665                            streetId: value && value.street_id ? String(value.street_id) : '',
5666                            label: String(text || '').trim(),
5667                            source: 'suggestion'
5668                        }, {
5669                            clearMap: true,
5670                            syncMirrorCityInputs: true,
5671                            includeStreetArrays: true,
5672                            clearIndexedCityArrays: true
5673                        });
5674                        this._syncCommittedLocationSearchLabel(text);
5675                    } else {
5676                        const ensureHidden = function (name) {
5677                            let $el = $form.find('[name="' + name + '"]');
5678                            if (!$el.length) {
5679                                $el = $('<input type="hidden"/>').attr('name', name).appendTo($form);
5680                            }
5681                            return $el;
5682                        };
5683                        ensureHidden('s').val(text || '');
5684                        const normalizedCityId = value && value.cities
5685                            ? Array.isArray(value.cities)
5686                                ? value.cities[0]
5687                                : value.cities
5688                            : '';
5689                        ensureHidden('city_id').val(normalizedCityId || '');
5690                        ensureHidden('district_id').val('');
5691                        ensureHidden('district_group_id').val('');
5692                        ensureHidden('street_id').val('');
5693                        ensureHidden('map').val('');
5694                    }
5695                    try {
5696                        const stateApi = window.MirelaSearch && window.MirelaSearch.searchStateApi;
5697                        const formEl = this.$form && this.$form[0] ? this.$form[0] : null;
5698                        if (stateApi && typeof stateApi.syncFromContext === 'function' && formEl) {
5699                            stateApi.syncFromContext({ form: formEl }, 'search-autocomplete.mobile-list-selection');
5700                        }
5701                    } catch (_eSliceMobileList) {}
5702                    // Do NOT auto-load offers here; user will press "Търси".
5703                    return; // stop further map-bound logic
5704                }
5705            } catch(_e) {}
5706
5707            // Handle location data if available
5708            // Home page: selecting from suggestions should immediately trigger a search request
5709            // (same effect as pressing the Search button), but ONLY on explicit user selection.
5710            // IMPORTANT: Always set this flag for home page, even if it's a second selection
5711            // This ensures that when user changes location (e.g., from Banichora to Varna), 
5712            // the search is triggered after the AJAX completes
5713            try {
5714                const isHome = isHomePageShell();
5715                const isExplicitClick = !!(e && e.type === 'click');
5716                if (isHome && isExplicitClick && !this._autoSelecting) {
5717                    this._homeAutoSearchPending = true;
5718                }
5719            } catch (_e) {}
5720
5721            if (value && (value.cities || value.districts || value.street_id || value.district_groups)) {
5722                this.handleLocationSelection(value, text);
5723            } else {
5724                // If there's no structured value (rare), still allow home to search immediately.
5725                try {
5726                    if (this._homeAutoSearchPending) {
5727                        this._homeAutoSearchPending = false;
5728                        const btn = document.querySelector('.js-map-manual');
5729                        if (btn) { try { $(btn).trigger('click'); } catch(_e) {} }
5730                    }
5731                } catch(_e) {}
5732            }
5733        }
5734
5735        handleLocationSelection(value, text) {
5736            const ajaxData = {
5737                term: text,
5738                get_polygons: true
5739            };
5740            try {
5741                const legacyLocationCompat = window.MirelaLocationFormLegacyCompat || null;
5742                if (legacyLocationCompat && typeof legacyLocationCompat.clearCommittedLocationContext === 'function' && this.$form && this.$form.length) {
5743                    legacyLocationCompat.clearCommittedLocationContext(this.$form, {
5744                        includeSearch: false,
5745                        clearMap: true,
5746                        removeMapManual: true
5747                    });
5748                }
5749            } catch (_eClearLocCtx) {}
5750            this._syncCommittedLocationSearchLabel(text);
5751            const hasPreciseLocation = !!(value && (value.districts || value.district_groups || value.street_id));
5752            // Normalize city id (first value if array)
5753            const normalizedCityId = Array.isArray(value.cities) ? value.cities[0] : value.cities;
5754            
5755            // NOTE: _homeAutoSearchPending is set in handleSuggestionClick, not here
5756            // This prevents duplicate flag setting and ensures it's only set on explicit user clicks
5757            
5758            if (value.districts) {
5759                ajaxData.district_id = value.districts;
5760                ajaxData.city_id = normalizedCityId ? [normalizedCityId] : undefined;
5761                $(this.options.selectors.districtId).val(value.districts);
5762                $(this.options.selectors.districtGroupId).val('');
5763                // Remove array-style clones from URL preservation
5764                this.$form.find('input[name="district_group_id[]"]').remove();
5765                this.$form.find('input[name="district_id[]"]').remove();
5766            } else if (value.district_groups) {
5767                ajaxData.district_group_id = value.district_groups;
5768                ajaxData.city_id = normalizedCityId ? [normalizedCityId] : undefined;
5769                $(this.options.selectors.districtGroupId).val(value.district_groups);
5770                $(this.options.selectors.districtId).val('');
5771                this.$form.find('input[name="district_group_id[]"]').remove();
5772                this.$form.find('input[name="district_id[]"]').remove();
5773            } else if (value.street_id) {
5774                ajaxData.street_id = value.street_id;
5775                ajaxData.city_id = normalizedCityId ? [normalizedCityId] : undefined;
5776                $(this.options.selectors.districtId).val('');
5777                $(this.options.selectors.districtGroupId).val('');
5778                // Persist street_id so the following map request includes it
5779                this.setOrCreateHidden('street_id', value.street_id);
5780                this.$form.find('input[name="district_group_id[]"]').remove();
5781                this.$form.find('input[name="district_id[]"]').remove();
5782            } else if (value.cities) {
5783                ajaxData.city_id = normalizedCityId ? [normalizedCityId] : undefined;
5784                $(this.options.selectors.districtId).val('');
5785                $(this.options.selectors.districtGroupId).val('');
5786                this.$form.find('input[name="district_group_id[]"]').remove();
5787                this.$form.find('input[name="district_id[]"]').remove();
5788            }
5789
5790            // Materialize structured location ids into real hidden fields so later home/list submit
5791            // flows do not depend on pre-existing district/group/street inputs in the DOM.
5792            this._syncStructuredLocationHiddenFields(value, normalizedCityId || '');
5793            try {
5794                if (isListPageShell()) {
5795                    window.__listLocationSelectionFitNext = true;
5796                }
5797            } catch(_eFitFlag) {}
5798            if (hasPreciseLocation) {
5799                this._skipDropdownSelectionSyncOnce = true;
5800            }
5801            this.syncLocationDropdownSelectionFromForm();
5802            try { this._patchUiSessionAfterLocationPick(text, value, normalizedCityId); } catch (_eUiS) {}
5803            try {
5804                const isHomePick =
5805                    isHomePageShell();
5806                if (isHomePick && !isListPageShell() && value && (value.districts || value.district_groups || value.street_id)) {
5807                    this._clearMapHiddenForHomeStructuredPick();
5808                }
5809            } catch (_eClearMapStr) {}
5810            this.persistCurrentHomeSelectionState();
5811
5812            // List page city-only selection must stay canonical and city-driven.
5813            // Do not hydrate hidden viewport bounds from `/address`, otherwise later
5814            // filter/pagination/refresh flows can revive stale map-mode requests.
5815            try {
5816                const isListCityOnly =
5817                    isListPageShell() &&
5818                    !!normalizedCityId &&
5819                    !value.districts &&
5820                    !value.district_groups &&
5821                    !value.street_id;
5822                if (isListCityOnly) {
5823                    try { this.setOrCreateHidden('map', ''); } catch(_eMapClear) {}
5824                    try {
5825                        const formEl = this.$form && this.$form.get ? this.$form.get(0) : null;
5826                        if (formEl) {
5827                            const mapManualEl = formEl.querySelector('input[name="map_manual"]');
5828                            if (mapManualEl && mapManualEl.parentNode) {
5829                                mapManualEl.parentNode.removeChild(mapManualEl);
5830                            }
5831                        }
5832                        try {
5833                            const resolveUrl =
5834                                typeof window.__MIRELA_OFFERS_SEARCH_RESOLVE_URL === 'string'
5835                                    ? String(window.__MIRELA_OFFERS_SEARCH_RESOLVE_URL || '').trim()
5836                                    : '';
5837                            const msLc = window.MirelaSearch;
5838                            if (
5839                                resolveUrl &&
5840                                msLc &&
5841                                typeof msLc.collectMergedOfferSearchNavigatePairs === 'function' &&
5842                                typeof msLc.postOffersSearchResolveCanonical === 'function' &&
5843                                formEl
5844                            ) {
5845                                const pairsLc = msLc.collectMergedOfferSearchNavigatePairs(formEl);
5846                                msLc.postOffersSearchResolveCanonical(resolveUrl, pairsLc)
5847                                    .then(function (href) {
5848                                        try {
5849                                            const u = new URL(href, window.location.origin);
5850                                            history.replaceState(
5851                                                null,
5852                                                '',
5853                                                (u.pathname || window.location.pathname) +
5854                                                    (u.search || '') +
5855                                                    (u.hash || '')
5856                                            );
5857                                        } catch (_eCanonUrl) {}
5858                                    })
5859                                    .catch(function () {
5860                                        try {
5861                                            const u = new URL(window.location.href);
5862                                            u.searchParams.delete('map');
5863                                            u.searchParams.delete('map_manual');
5864                                            history.replaceState(
5865                                                null,
5866                                                '',
5867                                                u.pathname + (u.search || '') + (u.hash || '')
5868                                            );
5869                                        } catch (_eFallbackUrl) {}
5870                                    });
5871                            } else {
5872                                try {
5873                                    const u = new URL(window.location.href);
5874                                    u.searchParams.delete('map');
5875                                    u.searchParams.delete('map_manual');
5876                                    history.replaceState(
5877                                        null,
5878                                        '',
5879                                        u.pathname + (u.search || '') + (u.hash || '')
5880                                    );
5881                                } catch (_eFallbackUrl2) {}
5882                            }
5883                        } catch (_eUrlSync) {}
5884                    } catch(_eMapManualClear) {}
5885                    try { window.__listBoundsMode = false; } catch(_eBounds) {}
5886                    try { window.__landedWithMap = false; } catch(_eLanded) {}
5887                    try { window.__mapManualNext = false; } catch(_eMapManualNext) {}
5888                    try { window.__listManualSearchNext = false; } catch(_eListManualNext) {}
5889                    try { window.__manualSearchRequested = false; } catch(_eManualReq) {}
5890                    try {
5891                        if (window.MirelaSearch && typeof window.MirelaSearch.applyUserMovedMapList === 'function') {
5892                            window.MirelaSearch.applyUserMovedMapList(false);
5893                        } else {
5894                            window.__mirelaUserMovedMap = false;
5895                        }
5896                    } catch(_eMoved) {}
5897                    try { window.__pendingLocationBounds = null; } catch(_ePendingBounds) {}
5898                    try { window.__pendingLocationBoundsTs = 0; } catch(_ePendingTs) {}
5899                    try {
5900                        const ui = window.MirelaSearch && window.MirelaSearch.uiSession;
5901                        if (ui && typeof ui.patch === 'function') {
5902                            ui.patch({ map: '' });
5903                        }
5904                    } catch (_eUiMapClear) {}
5905                    try { this._reloadListAfterCityOnlySelection(); } catch (_eListCityReload) {}
5906                    return;
5907                }
5908            } catch (_eListCityOnly) {}
5909
5910            // Fetch bounds/polygons data and store it in the hidden map input.
5911            // Client requirement: do NOT move the map / render pins / trigger searches on 
5911selection.
5912            let hasLocMap = false;
5913            try {
5914                hasLocMap =
5915                    !!$(this.options.selectors.locationMap).length ||
5916                    !!(this.$form && this.$form.find('input[name="map"]').length);
5917            } catch (_eHasMap) {
5918                hasLocMap = false;
5919            }
5920            if (!hasLocMap) {
5921                // List pages without a .js-location-map (some legacy layouts): hidden ids are already set;
5922                // still run /address + list refresh so municipality / district_group selections work.
5923                try {
5924                    if (
5925                        isListPageShell() &&
5926                        (value.districts || value.district_groups || value.street_id)
5927                    ) {
5928                        const self = this;
5929                        $.ajax({
5930                            url: this.options.ajaxUrl,
5931                            dataType: 'json',
5932                            data: ajaxData,
5933                            success: function (response) {
5934                                try {
5935                                    self._dispatchBrowseAfterLocationSelection();
5936                                } catch (_eLo) {}
5937                                try {
5938                                    self.persistCurrentHomeSelectionState();
5939                                } catch (_ePer) {}
5940                            },
5941                            error: function () {
5942                                try {
5943                                    self._dispatchBrowseAfterLocationSelection();
5944                                } catch (_eLo2) {}
5945                            }
5946                        });
5947                    }
5948                } catch (_eListNoMap) {}
5949                return;
5950            }
5951            $.ajax({
5952                url: this.options.ajaxUrl,
5953                dataType: 'json',
5954                data: ajaxData,
5955                success: (response) => {
5956                    try {
5957                        this.handleLocationResponse(response, value);
5958                    } catch (err) {
5959                        // Ако има грешка в MapLeaflet, не чупи autocomplete
5960                        // console.error('Map update error:', err);
5961                    }
5962                    this.persistCurrentHomeSelectionState();
5963                    try { this._homeAutoSearchPending = false; } catch(_ePendingReset) {}
5964                    this._dispatchBrowseAfterLocationSelection();
5965                },
5966                error: (xhr, status, error) => {
5967                    // console.error('AJAX error (location):', error);
5968                    this.persistCurrentHomeSelectionState();
5969                    try { this._homeAutoSearchPending = false; } catch(_e) {}
5970                }
5971            });
5972        }
5973
5974        handleLocationResponse(response, value) {
5975            try {
5976                const isHomeResp =
5977                    isHomePageShell();
5978                const isListResp =
5979                    isListPageShell();
5980                const strictStructuredPick = !!(value && (value.districts || value.district_groups || value.street_id));
5981                const skipMapHiddenForSearch =
5982                    strictStructuredPick && (isHomeResp || isListResp);
5983
5984                if (skipMapHiddenForSearch) {
5985                    try { this._clearMapHiddenForHomeStructuredPick(); } catch (_ePreClear) {}
5986                }
5987
5988                const writeMapBounds = (b, preferredZoom) => {
5989                    try {
5990                        if (skipMapHiddenForSearch) return;
5991                        if (!b) return;
5992                        const zoomToUse = preferredZoom || b.zoom || 14;
5993                        const mapStr = [b.ne_lng, b.ne_lat, b.sw_lng, b.sw_lat, zoomToUse].join('|');
5994                        $(this.options.selectors.locationMap).val(mapStr);
5995                        try { this._patchUiSessionMap(mapStr); } catch (_eUiMap) {}
5996                        // Store pending bounds so we can optionally fit on explicit "Търси"
5997                        try { window.__pendingLocationBounds = b; window.__pendingLocationBoundsZoom = zoomToUse; window.__pendingLocationBoundsTs = Date.now(); } catch(_e) {}
5998                    } catch (_e) {}
5999                };
6000
6001                // IMPORTANT: Set autocomplete selection flag BEFORE moving map
6002                // This allows fitMapBoundsSafely to move the map even if user previously moved it
6003                try {
6004                    const isHome = isHomePageShell();
6005                    if (isHome) {
6006                        window.__mirelaAutocompleteSelection = true;
6007                        // Clear flag after a delay (give time for fitMapBoundsSafely to complete)
6008                        setTimeout(() => {
6009                            try { window.__mirelaAutocompleteSelection = false; } catch(_e) {}
6010                        }, 2000);
6011                    }
6012                } catch(_e) {}
6013
6014                // Handle district polygons
6015                if (response.district_polygons) {
6016                    // IMPORTANT: Render polygons first, then move map
6017                    try {
6018                        const isHome = isHomePageShell();
6019                        const applyPolys = window.MirelaSearch && window.MirelaSearch.applyDistrictPolygonsToMapLeaflet;
6020                        if (isHome && window.MapLeaflet && typeof applyPolys === 'function') {
6021                            applyPolys(window.MapLeaflet, response, { preserveMarkers: false });
6022                        }
6023                    } catch(_e) {}
6024                    
6025                    if (response.bounds) {
6026                        writeMapBounds(response.bounds);
6027                        // On home page, move map to the new location bounds
6028                        try {
6029                            const isHome = isHomePageShell();
6030                            if (isHome && window.MapLeaflet && typeof window.MapLeaflet.fitMapBoundsSafely === 'function') {
6031                                // Suppress moveend auto-search since we'll trigger manual search below
6032                                try { window.__suppressNextMapAuto = true; } catch(_e) {}
6033                                setTimeout(() => {
6034                                    try { window.__suppressNextMapAuto = false; } catch(_e) {}
6035                                }, 900);
6036                                window.MapLeaflet.fitMapBoundsSafely({ bounds: response.bounds });
6037                            }
6038                        } catch(_e) {}
6039                    }
6040                }
6041                // Handle point data (street or city)
6042                else if (response.point) {
6043                    const lat = parseFloat(response.point.lat);
6044                    const lng = parseFloat(response.point.lng);
6045                    const isCitySearch = !!(value && (Array.isArray(value.cities) ? value.cities.length : value.cities));
6046                    const isListPage = isListPageShell();
6047                    // Slightly more zoomed out for entire city coverage on list page
6048                    const preferredZoom = (isCitySearch && isListPage) ? 11 : null;
6049                    
6050                    // Do not render a temporary marker while loading map-browse-offers results.
6051                    
6052                    if (response.bounds) {
6053                        writeMapBounds(response.bounds, preferredZoom);
6054                        // On home page, move map to the new location bounds
6055                        try {
6056                            const isHome = isHomePageShell();
6057                            if (isHome && window.MapLeaflet && typeof window.MapLeaflet.fitMapBoundsSafely === 'function') {
6058                                // Suppress moveend auto-search since we'll trigger manual search below
6059                                try { window.__suppressNextMapAuto = true; } catch(_e) {}
6060                                setTimeout(() => {
6061                                    try { window.__suppressNextMapAuto = false; } catch(_e) {}
6062                                }, 900);
6063                                window.MapLeaflet.fitMapBoundsSafely({ bounds: response.bounds, point: response.point });
6064                            }
6065                        } catch(_e) {}
6066                    } else {
6067                        // Compute reasonable fallback bounds around the point and write them to map input
6068                        const delta = preferredZoom ? 0.05 : 0.05; // a bit wider for city on list page
6069                        const fallbackBounds = {
6070                            sw_lat: lat - delta,
6071                            sw_lng: lng - delta,
6072                            ne_lat: lat + delta,
6073                            ne_lng: lng + delta,
6074                            zoom: preferredZoom || 12
6075                        };
6076                        writeMapBounds(fallbackBounds);
6077                        // On home page, move map to the new location bounds
6078                        try {
6079                            const isHome = isHomePageShell();
6080                            if (isHome && window.MapLeaflet && typeof window.MapLeaflet.fitMapBoundsSafely === 'function') {
6081                                // Suppress moveend auto-search since we'll trigger manual search below
6082                                try { window.__suppressNextMapAuto = true; } catch(_e) {}
6083                                setTimeout(() => {
6084                                    try { window.__suppressNextMapAuto = false; } catch(_e) {}
6085                                }, 900);
6086                                window.MapLeaflet.fitMapBoundsSafely({ bounds: fallbackBounds });
6087                            }
6088                        } catch(_e) {}
6089                    }
6090                }
6091                // Set city ID. Some street suggestions omit value.cities but /address returns context_city_id.
6092                const responseCityId = response && response.context_city_id != null ? String(response.context_city_id || '') : '';
6093                const valueCityId = Array.isArray(value.cities) ? (value.cities[0] || '') : (value.cities || '');
6094                this.setOrCreateHidden('city_id', valueCityId || responseCityId);
6095                // Do NOT trigger any change-driven auto-search here - it's handled in handleLocationSelection after AJAX completes
6096            } catch (err) {
6097                // Ако има грешка в MapLeaflet, autocomplete продължава да работи
6098                // console.error('MapLeaflet error:', err);
6099            }
6100        }
6101
6102        // If auto-search controls are not yet present (early user interaction),
6103        // trigger a manual search as soon as the controls appear, with a short timeout.
6104        triggerManualWhenControlsReady(maxWaitMs = 1500) {
6105            try {
6106                const autoCtrl = document.querySelector('.js-map-auto input[type="checkbox"]');
6107                if (autoCtrl) {
6108                    // If auto-search exists but is disabled, force a manual search
6109                    if (!autoCtrl.checked) {
6110                        const btn = document.querySelector('.js-map-manual');
6111                        if (btn) {
6112                            try { $(btn).trigger('click'); } catch(_e) {}
6113                            return;
6114                        }
6115                        // Manual button not yet available – keep waiting below
6116                    } else {
6117                        // Auto is enabled; MapLeaflet.fitMapBoundsSafely will call loadMap on moveend
6118                        return;
6119                    }
6120                }
6121            } catch (_e) {}
6122            const startTs = Date.now();
6123            const tryClick = () => {
6124                const btn = document.querySelector('.js-map-manual');
6125                if (btn) {
6126                    try { $(btn).trigger('click'); } catch(_e) {}
6127                } else if (Date.now() - startTs < maxWaitMs) {
6128                    setTimeout(tryClick, 80);
6129                }
6130            };
6131            setTimeout(tryClick, 150);
6132        }
6133
6134        setOrCreateHidden(name, value) {
6135            if (!this.$form || !name) return;
6136            const $all = this.$form.find(`[name="${name}"]`);
6137            if ($all.length) {
6138                $all.val(value);
6139            } else {
6140                const $el = $('<input type="hidden"/>').attr('name', name);
6141                this.$form.append($el);
6142                $el.val(value);
6143            }
6144            if (name === 'city_id' && value) {
6145                try { document.dispatchEvent(new CustomEvent('quickLinks:cityChanged', { bubbles: true, detail: { contextCityId: value } })); } catch (_e) {}
6146            }
6147        }
6148
6149        _syncSuggestionSelectionToFilterState(value, text) {
6150            try {
6151                if (typeof window.writeLocationFilterState !== 'function') return;
6152                const normalizedCityId = Array.isArray(value && value.cities) ? value.cities[0] : (value && value.cities);
6153                window.writeLocationFilterState({
6154                    label: String(text == null ? '' : text).trim(),
6155                    cityId: normalizedCityId ? String(normalizedCityId) : '',
6156                    districtIds: value && value.districts
6157                        ? (Array.isArray(value.districts) ? value.districts : [value.districts]).map(String).filter(
6157Boolean)
6158                        : [],
6159                    groupIds: value && value.district_groups
6160                        ? (Array.isArray(value.district_groups) ? value.district_groups : [value.district_groups]).map(String).filter(Boolean)
6161                        : [],
6162                    streetId: value && value.street_id ? String(value.street_id) : '',
6163                }, {
6164                    clearMap: true
6165                });
6166                try {
6167                    document.dispatchEvent(new CustomEvent('mirela:filters-state-change', {
6168                        bubbles: true,
6169                        detail: { type: 'suggestion-location', source: 'search-autocomplete' }
6170                    }));
6171                } catch (_eEv) {}
6172            } catch (_e) {}
6173        }
6174
6175        _syncStructuredLocationHiddenFields(value, normalizedCityId) {
6176            try {
6177                if (!this.$form) return;
6178                var selectionApi = window.MirelaLocationSelection || null;
6179                var forms = [];
6180                try {
6181                    var primary = this.$form && this.$form.get ? this.$form.get(0) : null;
6182                    if (primary) forms.push(primary);
6183                    [
6184                        document.querySelector('.js-search-form'),
6185                        document.querySelector('.js-mobile-search-form')
6186                    ].forEach(function (formEl) {
6187                        if (!formEl || forms.indexOf(formEl) !== -1) return;
6188                        forms.push(formEl);
6189                    });
6190                } catch (_eForms) {}
6191                var selection = {
6192                    cityId: normalizedCityId || '',
6193                    districtIds: value && value.districts
6194                        ? (Array.isArray(value.districts) ? value.districts : [value.districts])
6195                        : [],
6196                    groupIds: value && value.district_groups
6197                        ? (Array.isArray(value.district_groups) ? value.district_groups : [value.district_groups])
6198                        : [],
6199                    streetId: value && value.street_id ? String(value.street_id) : '',
6200                    label: (function () {
6201                        try {
6202                            var existing = (this.$form.find('input[name="s"]').val() || '').toString().trim();
6203                            return existing || (this.$input && this.$input.val ? String(this.$input.val() || '').trim() : '');
6204                        } catch (_eLabel) {
6205                            return '';
6206                        }
6207                    }).call(this),
6208                    source: 'suggestion'
6209                };
6210                this._syncSuggestionSelectionToFilterState(value, selection.label);
6211                if (selectionApi && typeof selectionApi.mirrorLocationToForms === 'function') {
6212                    selectionApi.mirrorLocationToForms(selection, forms, {
6213                        clearMap: true,
6214                        syncMirrorCityInputs: true,
6215                        includeStreetArrays: true,
6216                        clearIndexedCityArrays: true
6217                    });
6218                } else {
6219                    this.setOrCreateHidden('city_id', normalizedCityId || '');
6220                    try {
6221                        this.$form.find('input[name="district_id[]"], input[name="district_group_id[]"], input[name="city_id[]"]').remove();
6222                    } catch (_eArr) {}
6223                    if (value && value.districts) {
6224                        this.setOrCreateHidden('district_id', Array.isArray(value.districts) ? value.districts.join(',') : String(value.districts));
6225                        this.setOrCreateHidden('district_group_id', '');
6226                        this.setOrCreateHidden('street_id', '');
6227                    } else if (value && value.district_groups) {
6228                        this.setOrCreateHidden('district_group_id', Array.isArray(value.district_groups) ? value.district_groups.join(',') : String(value.district_groups));
6229                        this.setOrCreateHidden('district_id', '');
6230                        this.setOrCreateHidden('street_id', '');
6231                    } else if (value && value.street_id) {
6232                        this.setOrCreateHidden('street_id', String(value.street_id));
6233                        this.setOrCreateHidden('district_id', '');
6234                        this.setOrCreateHidden('district_group_id', '');
6235                    } else {
6236                        this.setOrCreateHidden('district_id', '');
6237                        this.setOrCreateHidden('district_group_id', '');
6238                        this.setOrCreateHidden('street_id', '');
6239                    }
6240                }
6241            } catch (_e) {}
6242        }
6243
6244        _dispatchBrowseAfterLocationSelection() {
6245            try {
6246                if (typeof window.dispatchBrowseByPageType === 'function') {
6247                    window.dispatchBrowseByPageType({
6248                        mapOnlyOptions: { defer: false }
6249                    });
6250                }
6251            } catch (_eDispatchBrowse) {}
6252        }
6253
6254        /** List city-only (София/Варна from autocomplete): canonical URL sync must still reload offers. */
6255        _reloadListAfterCityOnlySelection() {
6256            try {
6257                if (!isListPageShell()) { return false; }
6258                try {
6259                    if (typeof window.resetPaginationFilterState === 'function') {
6260                        window.resetPaginationFilterState();
6261                    } else if (typeof window.writePaginationFilterState === 'function') {
6262                        window.writePaginationFilterState(1);
6263                    } else if (typeof window.filterState === 'object' && window.filterState) {
6264                        window.filterState.page = '1';
6265                    }
6266                } catch (_ePage) {}
6267                const formEl = this.$form && this.$form.get ? this.$form.get(0) : null;
6268                const listRuntime = window.MirelaSearch && window.MirelaSearch.listRuntime
6269                    ? window.MirelaSearch.listRuntime
6270                    : null;
6271                if (listRuntime && typeof listRuntime.makeRequest === 'function' && formEl) {
6272                    return !!listRuntime.makeRequest({
6273                        reason: 'search-autocomplete.listCityOnlyCanonical',
6274                        requestMode: 'location',
6275                        form: formEl,
6276                        force: true,
6277                        append: false,
6278                        resetPaging: true,
6279                        explicitLocation: true
6280                    });
6281                }
6282                if (typeof window.loadOffers === 'function') {
6283                    window.loadOffers(1, false, true);
6284                    return true;
6285                }
6286            } catch (_e) {}
6287            this._dispatchBrowseAfterLocationSelection();
6288            return false;
6289        }
6290
6291        /**
6292         * Keep hidden `s` aligned with the committed location label so later AJAX/canonical reads do not
6293         * fall back to a stale city-only string after a street/district/group suggestion.
6294         */
6295        _syncCommittedLocationSearchLabel(text) {
6296            try {
6297                const label = String(text == null ? '' : text).trim();
6298                this.setOrCreateHidden('s', label);
6299                const FS = window.MirelaLocationFormSync;
6300                if (FS && typeof FS.syncCompactMobileInput === 'function') {
6301                    FS.syncCompactMobileInput(label);
6302                }
6303            } catch (_e) {}
6304        }
6305
6306        /**
6307         * Keeps MirelaSearch.uiSession in sync so canonical list/AJAX reads session before possibly stale DOM.
6308         */
6309        _patchUiSessionAfterLocationPick(text, value, normalizedCityId) {
6310            try {
6311                const ui = window.MirelaSearch && window.MirelaSearch.uiSession;
6312                if (!ui || typeof ui.patch !== 'function') return;
6313                const p = {};
6314                if (text !== undefined && text !== null) {
6315                    p.s = String(text).trim();
6316                }
6317                const cid = normalizedCityId != null && normalizedCityId !== ''
6318                    ? String(normalizedCityId).trim()
6319                    : '';
6320                p.city_id = cid;
6321                if (value && value.districts) {
6322                    const d = value.districts;
6323                    p.district_id = Array.isArray(d) ? d : [d];
6324                    p.district_group_id = [];
6325                    p.street_id = '';
6326                } else if (value && value.district_groups) {
6327                    const g = value.district_groups;
6328                    p.district_group_id = Array.isArray(g) ? g : [g];
6329                    p.district_id = [];
6330                    p.street_id = '';
6331                } else if (value && value.street_id) {
6332                    p.street_id = String(value.street_id).trim();
6333                    p.district_id = [];
6334                    p.district_group_id = [];
6335                } else if (value && value.cities) {
6336                    p.district_id = [];
6337                    p.district_group_id = [];
6338                    p.street_id = '';
6339                }
6340                ui.patch(p);
6341            } catch (_e) {}
6342        }
6343
6344        _patchUiSessionMap(mapStr) {
6345            try {
6346                const ui = window.MirelaSearch && window.MirelaSearch.uiSession;
6347                if (!ui || typeof ui.patch !== 'function') return;
6348                const m = String(mapStr || '').trim();
6349                if (m) {
6350                    ui.patch({ map: m });
6351                }
6352            } catch (_e) {}
6353        }
6354
6355        /**
6356         * Home: picking district / group / street from autocomplete should not send prior viewport `map`
6357         * (e.g. after "Търси тук"); search uses ids + label only. Map UI can still be fitted from /address without this hidden.
6358         */
6359        _clearMapHiddenForHomeStructuredPick() {
6360            try {
6361                const isHome =
6362                    isHomePageShell();
6363                if (!isHome) return;
6364                try { if (isListPageShell()) return; } catch (_eList) {}
6365                try { this.setOrCreateHidden('map', ''); } catch (_eH) {}
6366                try { $(this.options.selectors.locationMap).val(''); } catch (_eLoc) {}
6367                try {
6368                    document.querySelectorAll(
6369                        '.js-search-form input[name="map"], .js-search-form .js-location-map,' +
6370                        ' .js-mobile-search-form input[name="map"], .js-mobile-search-form .js-location-map'
6371                    ).forEach(function (el) {
6372                        try { el.value = ''; } catch (_e2) {}
6373                    });
6374                } catch (_eAll) {}
6375                try {
6376                    const ui = window.MirelaSearch && window.MirelaSearch.uiSession;
6377                    if (ui && typeof ui.patch === 'function') {
6378                        ui.patch({ map: '' });
6379                    }
6380                } catch (_eUi) {}
6381                try {
6382                    var stateApiMap = window.MirelaSearch && window.MirelaSearch.searchStateApi;
6383                    if (stateApiMap && typeof stateApiMap.patchState === 'function') {
6384                        stateApiMap.patchState({ filters: { map: '', map_manual: '', zoom: '' } }, 'search-autocomplete.clearMapHiddenForHomeStructuredPick');
6385                    } else if (typeof window.filterState === 'object' && window.filterState) {
6386                        window.filterState.map = '';
6387                        window.filterState.map_manual = '';
6388                        window.filterState.zoom = '';
6389                    }
6390                } catch (_eSliceMap) {}
6391                try {
6392                    window.__pendingLocationBounds = null;
6393                    window.__pendingLocationBoundsZoom = undefined;
6394                    window.__pendingLocationBoundsTs = 0;
6395                } catch (_ePb) {}
6396            } catch (_e) {}
6397        }
6398
6399        persistCurrentHomeSelectionState() {}
6400
6401        syncLocationDropdownSelectionFromForm() {
6402            try {
6403                const skipSyncOnce = this._skipDropdownSelectionSyncOnce === true;
6404                this._skipDropdownSelectionSyncOnce = false;
6405                document.querySelectorAll('.location-dropdown').forEach((dropdown) => {
6406                    try {
6407                        if (typeof dropdown.__resetToRoot === 'function') dropdown.__resetToRoot();
6408                    } catch (_eReset) {}
6409                    try {
6410                        if (
6411                            !skipSyncOnce &&
6412                            this._preferredReopenTarget(dropdown.querySelector('.js-location-dd-input, .js-search-input')) !== 'history' &&
6413                            typeof dropdown.__syncSelectionFromCurrentState === 'function'
6414                        ) {
6415                            dropdown.__syncSelectionFromCurrentState();
6416                        }
6417                    } catch (_eSync) {}
6418                    try {
6419                        const menu = dropdown.querySelector('.location-dd-menu');
6420                        const toggle = dropdown.querySelector('.dd-toggle');
6421                        if (menu) menu.style.display = 'none';
6422                        if (toggle) toggle.setAttribute('aria-expanded', 'false');
6423                        dropdown.classList.remove('open');
6424                    } catch (_eClose) {}
6425                });
6426            } catch(_e) {}
6427        }
6428
6429        _firstUsableSuggestionItem($scope) {
6430            try {
6431                const $root = $scope && $scope.length ? $scope : this.$suggestions;
6432                return $root.find('.suggestion-item').filter(function () {
6433                    try {
6434                        const v = (($(this).attr('data-value') || '') + '').trim();
6435                        return !!(v && v !== 'undefined');
6436                    } catch (_e) {
6437                        return false;
6438                    }
6439                }).first();
6440            } catch (_e2) {
6441                return $();
6442            }
6443        }
6444
6445        /**
6446         * Free-text in the visible field without a resolved /address suggestion is forbidden:
6447         * clear the box and wipe orphan hidden location ids driven by autocomplete.
6448         */
6449        _rejectUncommittedAutocompleteText() {
6450            try {
6451                this.hasConfirmedSelection = false;
6452                try {
6453                    this.$input.val('');
6454                } catch (_eSet) {}
6455                try {
6456                    $(this.options.selectors.districtId).val('');
6457                    $(this.options.selectors.districtGroupId).val('');
6458                    $(this.options.selectors.locationMap).val('');
6459                    this.setOrCreateHidden('street_id', '');
6460                    this.setOrCreateHidden('city_id', '');
6461                } catch (_eHid) {}
6462                try {
6463                    const el = this.$input.get(0);
6464                    if (el) {
6465                        el.dispatchEvent(new Event('input', { bubbles: true }));
6466                    }
6467                } catch (_eEv) {}
6468                try {
6469                    this.syncLocationDropdownSelectionFromForm();
6470                } catch (_eDd) {}
6471            } catch (_e) {}
6472        }
6473
6474        handleOutsideClick(e) {
6475            // Keep suggestion panel state aligned with the visible field (modal vs bar) before any dismiss logic.
6476            try {
6477                const modalShown = typeof document !== 'undefined'
6478                    ? document.querySelector('#mobileLocationModal.show')
6479                    : null;
6480                if (modalShown) {
6481                    const modalInp = modalShown.querySelector(this.options.selectors.input);
6482                    if (modalInp) {
6483                        this._setActiveContext($(modalInp));
6484                    }
6485                }
6486            } catch (_e) {}
6487
6488            // Mobile location modal: never auto-select first suggestion on outside click.
6489            // Keep behavior deterministic; selection is confirmed only by explicit tap + OK.
6490            try {
6491                const modalOpen = !!(typeof document !== 'undefined'
6492                    && document.querySelector('#mobileLocationModal.show')
6493                    && this.$input
6494                    && this.$input.length
6495                    && this.$input.closest('#mobileLocationModal').length);
6496                if (modalOpen) {
6497                    if ($(e.target).closest(this.options.selectors.input + ', ' + this.options.selectors.suggestions).length) {
6498                        return;
6499                    }
6500                    // Same as cancelPendingAddressSuggestions: this.$suggestions can point at the wrong .group-ip;
6501                    // always clear the modal container + pending /address so a late response cannot reopen the list.
6502                    try {
6503                        if (typeof this.cancelPendingAddressSuggestions === 'function') {
6504                            this.cancelPendingAddressSuggestions();
6505                        } else {
6506                            this._collapseSuggestions(this.$suggestions);
6507                        }
6508                    } catch (_e) {}
6509                    return;
6510                }
6511            } catch(_e) {}
6512
6513            // Demands pages (kupuvachi/naemateli) have their own SEO redirect logic and custom handlers.
6514            // Auto-selecting the first suggestion on outside click can:
6515            // - trigger an unexpected redirect, and
6516            // - immediately close other dropdowns (e.g. .nice-select) because it fires a synthetic click.
6517            // So on Demands pages we ONLY hide suggestions on outside click.
6518            // When invoked from blur (e.target === document.body), defer hide so the click on a suggestion
6519            // can run first; otherwise blur would hide/empty the list before the click is handled.
6520            // Use a short delay (50ms) so the click event is always processed before we hide/empty.
6521            try {
6522                if (isDemandsPageShell()) {
6523                    if ($(e.target).closest(this.options.selectors.input + ', ' + this.options.selectors.suggestions).length) {
6524                        return;
6525                    }
6526                    const self = this;
6527                    if (e.target === document.body) {
6528                        setTimeout(function() {
6529                            try { self._collapseSuggestions(self.$suggestions); } catch(_err) { try { self._collapseSuggestions(self.$suggestions); } catch(_e2) {} }
6530                        }, 50);
6531                    } else {
6532                        try { this._collapseSuggestions(this.$suggestions); } catch(_e) { try { this._collapseSuggestions(this.$suggestions); } catch(_e2) {} }
6533                    }
6534                    return;
6535                }
6536            } catch (_e) {}
6537
6538            // Offer preview: do not commit on arbitrary page clicks (can fire alongside map/UI).
6539            // Blur is routed through handleOutsideClick({ target: document.body }) — allow commit there.
6540            try {
6541                if (isActualOfferPreviewPage()) {
6542                    if ($(e.target).closest(this.options.selectors.input + ', ' + this.options.selectors.suggestions).length) {
6543                        return;
6544                    }
6545                    if (e.target !== document.body) {
6546                        try { this._collapseSuggestions(this.$suggestions); } catch(_ePv) { try { this._collapseSuggestions(this.$suggestions); } catch(_ePv2) {} }
6547                        return;
6548                    }
6549                }
6550            } catch (_e) {}
6551
6552            if ($(e.target).closest(this.options.selectors.input + ', ' + this.options.selectors.suggestions).length) {
6553                return;
6554            }
6555            const term = (this.$input.val() || '').trim();
6556            // Label from DD menu / list SSR (hidden city_id et al.) is not "orphan typing".
6557            // Blur-used-for-outside-click must not fake a suggestion pick — that flips reopen target to `history`.
6558            const inputEl = this.$input && this.$input.length ? this.$input.get(0) : null;
6559            const committedStructuredLocation = !!(
6560                inputEl &&
6561                this._hasStructuredLocationSelection() &&
6562                !this._isCityIdOnlyWithoutVisibleSearchLabel(inputEl)
6563            );
6564            // Requirement: if user typed something and clicks outside without choosing,
6565            // auto-select the first suggestion, BUT do NOT auto-search.
6566            if (term.length >= this.options.minSearchLength && !this.hasConfirmedSelection && !committedStructuredLocation) {
6567                if (this._autoSelecting) { return; }
6568                this._autoSelecting = true;
6569                const $first = this._firstUsableSuggestionItem(this.$suggestions);
6570                if ($first && $first.length) {
6571                    try {
6572                        // Do NOT trigger a real click (it can bubble to page handlers).
6573                        this.handleSuggestionClick({ currentTarget: $first.get(0) });
6574                    } catch(_e) { try { this.handleSuggestionClick({ currentTarget: $first.get(0) }); } catch(_e2) {} }
6575                    this._autoSelecting = false;
6576                    try { this._collapseSuggestions(this.$suggestions); } catch(_e) { try { this._collapseSuggestions(this.$suggestions); } catch(_e2) {} }
6577                    return;
6578                }
6579                // No visible suggestions in DOM → fetch and pick first (even if typing was earlier;
6580                // otherwise free-text orphans would linger without a backing /address payload).
6581                $.ajax({
6582                    url: this.options.ajaxUrl,
6583                    dataType: 'json',
6584                    data: { term },
6585                    success: (data) => {
6586                        try {
6587                            if (Array.isArray(data) && data.length) {
6588                                const usableRows = [];
6589                                try {
6590                                    for (let i = 0; i < data.length; i++) {
6591                                        const row = data[i];
6592                                        if (row && row.value) { usableRows.push(row); }
6593                                    }
6594                                } catch (_eFil) {}
6595                                if (usableRows.length) {
6596                                    try { this.displaySuggestions(usableRows); } catch (_eDisp) {}
6597                                }
6598                                const $fi = this._firstUsableSuggestionItem(this.$suggestions);
6599                                if ($fi && $fi.length) {
6600                                    try { this.handleSuggestionClick({ currentTarget: $fi.get(0) }); } catch(_e) { try { this.handleSuggestionClick({ currentTarget: $fi.get(0) }); } catch(_e2) {} }
6601                                    this._autoSelecting = false;
6602                                    try { this._collapseSuggestions(this.$suggestions); } catch(_e) { try { this._collapseSuggestions(this.$suggestions); } catch(_e2) {} }
6603                                    return;
6604                                }
6605                            }
6606                        } catch(_e) {}
6607                        this._collapseSuggestions(this.$suggestions);
6608                        this._autoSelecting = false;
6609                        this._rejectUncommittedAutocompleteText();
6610                    },
6611                    error: () => {
6612                        try { this._collapseSuggestions(this.$suggestions); } catch(_eCe) {}
6613                        this._autoSelecting = false;
6614                        this._rejectUncommittedAutocompleteText();
6615                    }
6616                });
6617                return;
6618            }
6619            this._collapseSuggestions(this.$suggestions);
6620        }
6621
6622        handleInputChange(e) {
6623            const val = this.$input.val().trim();
6624            const isTrustedInput = !!(e && (e.isTrusted === true || (e.originalEvent && e.originalEvent.isTrusted === true)));
6625            try {
6626                if (Date.now() < (this.suppressUntilTs || 0)) {
6627                    if (val.length >= this.options.minSearchLength) {
6628                        this.hasConfirmedSelection = true;
6629                    }
6630                    return;
6631                }
6632                this._lastInputEventTs = Date.now();
6633            } catch (_e) {}
6634            this.hasConfirmedSelection = false;
6635            if (isTrustedInput) {
6636                this._resetPreferredReopenTarget();
6637            }
6638            if (val.length > 0) {
6639                this._setHistoryEligible(false);
6640            }
6641            try { window.__listLocationSelectionFitNext = false; } catch(_eFlag) {}
6642            // On any typing, clear previous street filter so it doesn't leak into next search
6643            this.setOrCreateHidden('street_id', '');
6644            if (val.length < this.options.minSearchLength) {
6645                $(this.options.selectors.districtId).val('');
6646                $(this.options.selectors.districtGroupId).val('');
6647                $(this.options.selectors.locationMap).val('');
6648                // Also clear city when the input is cleared
6649                this.setOrCreateHidden('city_id', '');
6650                // History list is click-toggled only; do not auto-open on clear/short input.
6651                this._collapseSuggestions(this.$suggestions);
6652            }
6653        }
6654
6655        handleFormSubmit(e) {
6656            const searchValue = String(this.$input && this.$input.val() ? this.$input.val() : '').trim();
6657            this._persistPreferredReopenTargetForCurrentState();
6658            if (/^\d+$/.test(searchValue) && searchValue.length > 0) {
6659                e.preventDefault();
6660                this._storeOfferSearchIfListPage(searchValue);
6661                const isListCtx = isListPageShell();
6662                if (isListCtx) {
6663                    this._prepareListFormForOfferIdSearch(searchValue);
6664                    this._resolveOfferUrlById(
6665                        searchValue,
6666                        (href) => {
6667                            try { window.location.href = href; } catch (_eNav) {}
6668                        },
6669                        () => {
6670                            this._redirectNumericOfferIdSearch(searchValue);
6671                        }
6672                    );
6673                } else {
6674                    this._redirectNumericOfferIdSearch(searchValue);
6675                }
6676                return;
6677            }
6678
6679            try {
6680                if (isDemandsPageShell()) {
6681                    // Demands pages own their redirect flow (including agent demands exit-to-normal-list).
6682                    // Do not treat them as generic "external pages".
6683                    return;
6684                }
6685            } catch (_eDemands) {}
6686            // External pages (e.g. /cenite): do not hijack submit.
6687            // Build canonical locale-aware list URLs directly here.
6688            try {
6689                const isHome = isHomePageShell();
6690                const isList = isListPageShell();
6691                if (!isHome && !isList) {
6692                    const form = this.$form && this.$form.length ? this.$form[0] : (this.$input && this.$input.length ? this.$input.closest('form')[0] : null);
6693                    if (!form) return;
6694                    e.preventDefault();
6695                    this._redirectExternalSearchToCanonicalList(form);
6696                    return;
6697                }
6698            } catch (_eCtx) {}
6699
6700            // Persist custom search (no AJAX autocomplete) in history
6701            try {
6702                // Build value payload from hidden inputs if available
6703                const $form = this.$form || this.$input.closest('form');
6704                const get = (n) => ($form && $form.find(`[name="${n}"]`).val()) || '';
6705                const cityId = get('city_id');
6706                const districtsCsv = get('district_id');
6707                const groupsCsv = get('district_group_id');
6708                const streetId = get('street_id');
6709                const value = {};
6710                if (cityId) { value.cities = Array.isArray(cityId) ? cityId : [cityId]; }
6711                if (districtsCsv) { value.districts = String(districtsCsv).split(',').filter(Boolean).map(s=>s.trim()); }
6712                if (groupsCsv) { value.district_groups = String(groupsCsv).split(',').filter(Boolean).map(s=>s.trim()); }
6713                if (streetId) { value.street_id = streetId; }
6714                const rec = { label: searchValue || (value.cities ? '—' : ''), typeahead: searchValue, suggestion: searchValue, value };
6715                const isAddress = !!(value.cities || value.districts || value.district_groups || value.street_id);
6716                const key = isAddress ? HISTORY.KEY_AUTOCOMPLETE_A : HISTORY.KEY_AUTOCOMPLETE;
6717                const limit = isAddress ? this.options.historyLimits.acA : this.options.historyLimits.ac;
6718                // Store only if 3+ chars OR address selection present
6719                if ((searchValue && searchValue.length > 3) || isAddress) {
6720                    HISTORY.put(key, rec, limit);
6721                }
6722            } catch(_e) {}
6723            try { if (window.__disableAjaxList) { return; } } catch(_e) {}
6724            // Home page: move map to pending bounds (if any), then search
6725            try {
6726                if (isHomePageShell()) {
6727                    e.preventDefault();
6728                    this._applyPendingBoundsAndSearch();
6729                    return;
6730                }
6731            } catch(_e) {}
6732            e.preventDefault();
6733            this.triggerManualSearch();
6734        }
6735
6736        handleSearchButtonClick(e) {
6737            try {
6738                const target = e && e.target && e.target.closest ? e.target.closest('.js-home-list-cta') : null;
6739                if (target) {
6740                    return;
6741                }
6742            } catch (_eHomeListCta) {}
6743
6744            const searchValueBtn = String(this.$input && this.$input.val() ? this.$input.val() : '').trim();
6745            this._persistPreferredReopenTargetForCurrentState();
6746            if (/^\d+$/.test(searchValueBtn) && searchValueBtn.length > 0) {
6747                e.preventDefault();
6748                this._storeOfferSearchIfListPage(searchValueBtn);
6749                const isListCtxBtn = isListPageShell();
6750                if (isListCtxBtn) {
6751                    this._prepareListFormForOfferIdSearch(searchValueBtn);
6752                    this._resolveOfferUrlById(
6753                        searchValueBtn,
6754                        (href) => {
6755                            try { window.location.href = href; } catch (_eNav) {}
6756                        },
6757                        () => {
6758                            this._redirectNumericOfferIdSearch(searchValueBtn);
6759                        }
6760                    );
6761                } else {
6762                    this._redirectNumericOfferIdSearch(searchValueBtn);
6763                }
6764                return;
6765            }
6766
6767            try {
6768                if (isDemandsPageShell()) {
6769                    // Let demands-specific handlers build the target URL.
6770                    return;
6771                }
6772            } catch (_eDemands) {}
6773            // External pages (e.g. /cenite): do not hijack click submit flow.
6774            // Build canonical locale-aware list URLs directly here.
6775            try {
6776                const isHome = isHomePageShell();
6777                const isList = isListPageShell();
6778                if (!isHome && !isList) {
6779                    const form = this.$form && this.$form.length ? this.$form[0] : (this.$input && this.$input.length ? this.$input.closest('form')[0] : null);
6780                    if (!form) return;
6781                    e.preventDefault();
6782                    this._redirectExternalSearchToCanonicalList(form);
6783                    return;
6784                }
6785            } catch (_eCtx) {}
6786
6787            // Home page: move map to pending bounds (if any), then search
6788            try {
6789                if (isHomePageShell()) {
6790                    e.preventDefault();
6791                    this._applyPendingBoundsAndSearch();
6792                    return;
6793                }
6794            } catch(_e) {}
6795            try { if (window.__disableAjaxList) { return; } } catch(_e) {}
6796            e.preventDefault();
6797            this.triggerManualSearch();
6798        }
6799
6800        _redirectExternalSearchToCanonicalList(form) {
6801            try {
6802                try {
6803                    var flushNav =
6804                        window.MirelaSearch &&
6805                        typeof window.MirelaSearch.flushOfferSearchNavigateFormDom === 'function'
6806                            ? window.MirelaSearch.flushOfferSearchNavigateFormDom
6807                            : null;
6808                    if (flushNav) flushNav(form);
6809                } catch (_eUnifiedFlushAc) {}
6810                try {
6811                    if (!(window.MirelaSearch && typeof window.MirelaSearch.flushOfferSearchNavigateFormDom === 'function')) {
6812                        try {
6813                            var SharedAdvAc = window.AdvancedSearchShared;
6814                            if (SharedAdvAc && typeof SharedAdvAc.syncAdvancedPanelStateToForm === 'function') {
6815                                SharedAdvAc.syncAdvancedPanelStateToForm({
6816                                    form: form,
6817                                    panel: document.querySelector('.wd-search-form')
6818                                });
6819                            }
6820                        } catch (_eAcFlush) {}
6821                        try {
6822                            var trfAc = window.MirelaSearch && window.MirelaSearch.topRangeFilters;
6823                            if (trfAc && typeof trfAc.flushRibbonRangesIntoForm === 'function') {
6824                                trfAc.flushRibbonRangesIntoForm(form);
6825                            }
6826                        } catch (_eRibbonFlushAc) {}
6827                    }
6828                } catch (_eLegacyFlushFallback) {}
6829                const msExt = window.MirelaSearch;
6830                if (msExt && typeof msExt.tryOffersSearchServerListNavigationFromForm === 'function') {
6831                    const navExt = msExt.tryOffersSearchServerListNavigationFromForm(form);
6832                    if (navExt && navExt.handled) {
6833                        if (!navExt.sync && navExt.promise && typeof navExt.promise.catch === 'function') {
6834                            navExt.promise.catch(function () {
6835                                try {
6836                                    form.submit();
6837                                } catch (__e) {}
6838                            });
6839                        }
6840                        return;
6841                    }
6842                }
6843            } catch(_e) {
6844                try { form.submit(); } catch(__e) {}
6845            }
6846            try { form.submit(); } catch(__es) {}
6847        }
6848
6849        // Helper: Store offer ID so preview page can restore it in the search input
6850        // (used when navigating to offer by number from home, list, or any page)
6851        _storeOfferSearchIfListPage(offerId) {
6852            try {
6853                if (typeof window !== 'undefined' && offerId) {
6854                    const offerHint = window.MirelaSearch && window.MirelaSearch.offerIdSearchHint;
6855                    if (offerHint && typeof offerHint.set === 'function') offerHint.set(String(offerId));
6856                }
6857            } catch(_e) {}
6858        }
6859
6860        _redirectNumericOfferIdSearch(offerId) {
6861            try {
6862                const id = String(offerId || '').trim();
6863                if (!id) return;
6864                const submitController =
6865                    window.MirelaSearch && window.MirelaSearch.searchSubmitController
6866                        ? window.MirelaSearch.searchSubmitController
6867                        : null;
6868                if (submitController && typeof submitController.resolveOfferRedirectUrl === 'function') {
6869                    const href = String(
6870                        submitController.resolveOfferRedirectUrl(id, window.location.pathname || '') || ''
6871                    ).trim();
6872                    if (href) {
6873                        window.location.href = href;
6874                        return;
6875                    }
6876                }
6877            } catch (_e) {}
6878            try {
6879                window.location.href = '/offer/' + encodeURIComponent(String(offerId || '').trim());
6880            } catch (_e2) {}
6881        }
6882
6883        _prepareListFormForOfferIdSearch(offerId) {
6884            try {
6885                const form = document.querySelector('.js-search-form');
6886                if (!form) return;
6887                const clearScalar = (name) => {
6888                    try {
6889                        const el = form.querySelector(`input[name="${name}"]`);
6890                        if (el) el.value = '';
6891                    } catch(_e) {}
6892                };
6893                clearScalar('city_id');
6894                clearScalar('district_id');
6895                clearScalar('district_group_id');
6896                clearScalar('street_id');
6897                clearScalar('map');
6898
6899                try {
6900                    const mapMirror = form.querySelector('.js-location-map');
6901                    if (mapMirror) mapMirror.value = '';
6902                } catch(_eMap) {}
6903
6904                try {
6905                    form.querySelectorAll('input[name="district_id[]"], input[name="district_group_id[]"], input[name="city_id[]"]').forEach((el) => {
6906                        try { el.remove(); } catch(_eRm) {}
6907                    });
6908                } catch(_eArr) {}
6909
6910                try {
6911                    let sEl = form.querySelector('input[name="s"]');
6912                    if (!sEl) {
6913                        sEl = document.createElement('input');
6914                        sEl.type = 'hidden';
6915                        sEl.name = 's';
6916                        form.appendChild(sEl);
6917                    }
6918                    sEl.value = String(offerId || '').trim();
6919                } catch(_eS) {}
6920            } catch(_e) {}
6921        }
6922
6923        _collectSearchFormPayload() {
6924            const payload = {};
6925            try {
6926                const form = document.querySelector('.js-search-form');
6927                if (!form) return payload;
6928                const fd = new FormData(form);
6929                for (const [k, v] of fd.entries()) {
6930                    const key = String(k || '').trim();
6931                    if (!key || key === '_token') continue;
6932                    const val = (v == null) ? '' : String(v).trim();
6933                    if (!val) continue;
6934                    if (Object.prototype.hasOwnProperty.call(payload, key)) {
6935                        if (Array.isArray(payload[key])) payload[key].push(val);
6936                        else payload[key] = [payload[key], val];
6937                    } else {
6938                        payload[key] = val;
6939                    }
6940                }
6941            } catch(_e) {}
6942            return payload;
6943        }
6944
6945        _extractOfferHrefFromListHtml(listHtml, offerId) {
6946            try {
6947                const id = String(offerId || '').trim();
6948                if (!id || !listHtml) return '';
6949                const holder = document.createElement('div');
6950                holder.innerHTML = String(listHtml || '');
6951                const byCard = holder.querySelector(`[data-offer-id="${id.replace(/"/g, '')}"] a[href]`);
6952                if (byCard && byCard.getAttribute) {
6953                    const href = String(byCard.getAttribute('href') || '').trim();
6954                    if (href) return href;
6955                }
6956                const bySlug = holder.querySelector(`a[href*="-${id}"], a[href*="/offer/${id}"]`);
6957                if (bySlug && bySlug.getAttribute) {
6958                    const href2 = String(bySlug.getAttribute('href') || '').trim();
6959                    if (href2) return href2;
6960                }
6961            } catch(_e) {}
6962            return '';
6963        }
6964
6965        _resolveOfferUrlById(offerId, onResolved, onFallback) {
6966            try {
6967                if (window.__offerIdResolveInProgress) {
6968                    if (typeof onFallback === 'function') onFallback();
6969                    return;
6970                }
6971                window.__offerIdResolveInProgress = true;
6972                const payload = this._collectSearchFormPayload();
6973                payload.s = String(offerId || '').trim();
6974                try { delete payload.map; } catch(_eMap) {}
6975                try { delete payload.map_manual; } catch(_eMap2) {}
6976                const finish = () => { try { window.__offerIdResolveInProgress = false; } catch(_eFlag) {} };
6977                window.sendMapBrowseOffers({
6978                    payload,
6979                    success: (resp) => {
6980                        let href = '';
6981                        try { href = this._extractOfferHrefFromListHtml(resp && resp.list_html, offerId); } catch(_eHref) {}
6982                        finish();
6983                        if (href && typeof onResolved === 'function') { onResolved(href); return; }
6984                        if (typeof onFallback === 'function') onFallback();
6985                    }
6986                }).fail(() => {
6987                    finish();
6988                    if (typeof onFallback === 'function') onFallback();
6989                });
6990            } catch(_e) {
6991                try { window.__offerIdResolveInProgress = false; } catch(_eFlag2) {}
6992                if (typeof onFallback === 'function') onFallback();
6993            }
6994        }
6995
6996        // Apply pending location bounds (from autocomplete selection) to the map and trigger search.
6997        // Used by both home and list pages when "Търси" is pressed.
6998        _applyPendingBoundsAndSearch() {
6999            // Home: polygon location (district / group / street) should open the SEO list path
7000            // (e.g. /prodazhbi/grad-sofia/mladost) via submit-controller rollup — not only map-browse.
7001            try {
7002                if (isHomePageShell()) {
7003                    const form = document.querySelector('.js-search-form');
7004                    if (form) {
7005                        const getV = (sel) => {
7006                            try {
7007                                return String((form.querySelector(sel) && form.querySelector(sel).value) || '').trim();
7008                            } catch (_e) {
7009                                return '';
7010                            }
7011                        };
7012                        const hasDistrict =
7013                            getV('input[name="district_id"]') !== '' ||
7014                            form.querySelectorAll('input[name="district_id[]"]').length > 0;
7015                        const hasGroup =
7016                            getV('input[name="district_group_id"]') !== '' ||
7017                            form.querySelectorAll('input[name="district_group_id[]"]').length > 0;
7018                        const hasStreet = getV('input[name="street_id"]') !== '';
7019                        if (hasDistrict || hasGroup || hasStreet) {
7020                            const msH = window.MirelaSearch;
7021                            const hForm = document.querySelector('.js-search-form');
7022                            if (
7023                                msH &&
7024                                typeof msH.tryOffersSearchServerListNavigationFromForm === 'function' &&
7025                                hForm
7026                            ) {
7027                                const navH = msH.tryOffersSearchServerListNavigationFromForm(hForm);
7028                                if (navH && navH.handled) {
7029                                    if (!navH.sync && navH.promise && typeof navH.promise.catch === 'function') {
7030                                        navH.promise.catch(function () {});
7031                                    }
7032                                    return;
7033                                }
7034                            }
7035                        }
7036                    }
7037                }
7038            } catch (_eHomeList) {}
7039            try {
7040                const pb = window.__pendingLocationBounds;
7041                const pbTs = window.__pendingLocationBoundsTs || 0;
7042                if (pb && (Date.now() - pbTs < 300000)) {
7043                    window.__pendingLocationBounds = null;
7044                    window.__pendingLocationBoundsTs = 0;
7045                    const zoom = window.__pendingLocationBoundsZoom || pb.zoom || 14;
7046                    // Write bounds to hidden map input
7047                    try {
7048                        const mapEl = document.querySelector('.js-search-form input[name="map"]');
7049                        if (mapEl) {
7050                            mapEl.value = [pb.ne_lng, pb.ne_lat, pb.sw_lng, pb.sw_lat, zoom].join('|');
7051                        }
7052                    } catch(_e) {}
7053                    // Move the map and trigger search via moveend
7054                    if (typeof window.MapLeaflet !== 'undefined' && window.MapLeaflet.fitMapBoundsSafely) {
7055                        // User explicitly pressed "Търси" with pending location bounds.
7056                        // Signal autocomplete selection so fitMapBoundsSafely does NOT
7057                        // skip the fit even when user previously dragged the map.
7058                        try { window.__mirelaAutocompleteSelection = true; } catch(_e) {}
7059                        try { window.__forceHomeAutoFitOnce = true; } catch(_e) {}
7060                        try {
7061                            window.MapLeaflet.fitMapBoundsSafely({ bounds: { ...pb, zoom: zoom } });
7062                        } catch(_e) {}
7063                        try { setTimeout(function(){ window.__mirelaAutocompleteSelection = false; }, 2000); } catch(_e) {}
7064                        return;
7065                    }
7066                }
7067            } catch(_e) {}
7068            // No pending bounds — fall back to manual button
7069            const manualBtn = document.querySelector('.js-map-manual');
7070            if (manualBtn) {
7071                try { manualBtn.click(); } catch(_e) {}
7072            }
7073        }
7074
7075        triggerManualSearch() {
7076            try { if (window.__disableAjaxList) {
7077                const form = document.querySelector('.js-search-form');
7078                if (form) {
7079                    try { if ((form.method||'').toLowerCase() !== 'get') { form.method = 'get'; } } catch(_e){}
7080                    try { var act = form.getAttribute('action'); if (!act || act === 'javascript:void(0)') { form.setAttribute('action', window.location.pathname); } } catch(_e){}
7081                    if (makeSearchQuery('submit', { form: form, force: true }) === false) {
7082                        if (typeof form.requestSubmit === 'function') { form.requestSubmit(); }
7083                        else { form.submit(); }
7084                    }
7085                }
7086                return;
7087            } } catch(_e) {}
7088
7089            // List page: handle location-based searches properly.
7090            try {
7091                if (isListPageShell()) {
7092                    const form = document.querySelector('.js-search-form');
7093                    if (form) {
7094                        const listRuntime = window.MirelaSearch && window.MirelaSearch.listRuntime
7095                            ? window.MirelaSearch.listRuntime
7096                            : null;
7097                        try {
7098                            const shared = window.AdvancedSearchShared || null;
7099                            if (shared && typeof shared.syncDefaultAdvancedPanelStateToForm === 'function') {
7100                                shared.syncDefaultAdvancedPanelStateToForm({
7101                                    panel: document.querySelector('.wd-search-form'),
7102                                    form: form
7103                                });
7104                            } else if (shared && typeof shared.syncAdvancedPanelStateToForm === 'function') {
7105                                shared.syncAdvancedPanelStateToForm({
7106                                    panel: document.querySelector('.wd-search-form'),
7107                                    form: form
7108                                });
7109                            }
7110                        } catch (_eSyncPanel) {}
7111                        const getVal = (sel) => {
7112                            try { return String((form.querySelector(sel)?.value || '')).trim(); } catch(_e){ return ''; }
7113                        };
7114                        const hasDistrict = getVal('[name="district_id"]') !== '' || form.querySelectorAll('input[name="district_id[]"]').length > 0;
7115                        const hasGroup    = getVal('[name="district_group_id"]') !== '' || form.querySelectorAll('input[name="district_group_id[]"]').length > 0;
7116                        const hasStreet   = getVal('[name="street_id"]') !== '';
7117                        const hasCityId   = getVal('[name="city_id"]') !== '';
7118                        const hasExplicitLocation = hasDistrict || hasGroup || hasStreet || hasCityId;
7119
7120                        if (listRuntime && typeof listRuntime.makeRequest === 'function') {
7121                            try {
7122                                listRuntime.makeRequest({
7123                                    reason: 'search-autocomplete.triggerManualSearch',
7124                                    requestMode: hasExplicitLocation ? 'location' : undefined,
7125                                    form: form,
7126                                    force: true,
7127                                    append: false,
7128                                    resetPaging: true,
7129                                    explicitLocation: hasExplicitLocation
7130                                });
7131                                return;
7132                            } catch (_eListRuntime) {}
7133                        }
7134
7135                        // Polygon-based filter (district/group/street): use loadOffers path
7136                        if (hasDistrict || hasGroup || hasStreet) {
7137                            // Clear stale map bounds — polygon search doesn't use viewport
7138                            try {
7139                                const mapEl = form.querySelector('input[name="map"]');
7140                                if (mapEl) { mapEl.value = ''; }
7141                                const mapEl2 = form.querySelector('.js-location-map');
7142                                if (mapEl2) { mapEl2.value = ''; }
7143                            } catch(_e) {}
7144                            // Strip stale ?map=/?map_manual= from URL so prepareListFormMapForRequest
7145                            // does not re-inject Leaflet viewport bounds via the hasUrlMap branch.
7146                            try {
7147                                const _u = new URL(window.location.href);
7148                                if (_u.searchParams.has('map') || _u.searchParams.has('map_manual')) {
7149                                    _u.searchParams.delete('map');
7150                                    _u.searchParams.delete('map_manual');
7151                                    history.replaceState(null, '', _u.pathname + (_u.search || '') + (_u.hash || ''));
7152                                }
7153                            } catch(_eStripMapUrl) {}
7154                            // Exit bounds mode and force map to fit response bounds
7155                            try { window.__listBoundsMode = false; } catch(_e) {}
7156                            try { window.__landedWithMap = false; } catch(_e) {}
7157                            try {
7158                                if (window.MirelaSearch && typeof window.MirelaSearch.applyUserMovedMapList === 'function') {
7159                                    window.MirelaSearch.applyUserMovedMapList(false);
7160                                } else {
7161                                    window.__mirelaUserMovedMap = false;
7162                                }
7163                            } catch (_e) {}
7164                            try { window.__mapManualNext = false; } catch(_e) {}
7165                            try { window.__listForceFitOnce = true; } catch(_e) {}
7166                            try { window.__listLocationSelectionFitNext = true; } catch(_e) {}
7167                            // Consume pending bounds (server returns correct ones)
7168                            try {
7169                                window.__pendingLocationBounds = null;
7170                                window.__pendingLocationBoundsTs = 0;
7171                            } catch(_e) {}
7172                            const triggerEl =
7173                                form.querySelector('input[name="district_id"]') ||
7174                                form.querySelector('input[name="district_group_id"]') ||
7175                                form.querySelector('input[name="street_id"]') ||
7176                                form.querySelector('input[name="type"]');
7177                            if (triggerEl) {
7178                                try { triggerEl.dispatchEvent(new Event('change', { bubbles: true })); } catch(_e) {}
7179                                return;
7180                            }
7181                        }
7182
7183                        // If the user selected a location (city/district) from autocomplete:
7184                        // Clear stale map bounds, exit bounds mode, force-fit after response,
7185                        // and use loadOffers with the city_id. The server returns the correct
7186                        // bounds in the response, and forceFitOnce makes the map move there.
7187                        if (hasCityId && !hasDistrict && !hasGroup && !hasStreet) {
7188                            // Clear stale map bounds so loadOffers doesn't use old viewport
7189                            try {
7190                                const mapEl = form.querySelector('input[name="map"]');
7191                                if (mapEl) { mapEl.value = ''; }
7192                                const mapEl2 = form.querySelector('.js-location-map');
7193                                if (mapEl2) { mapEl2.value = ''; }
7194                            } catch(_e) {}
7195                            // Strip stale ?map=/?map_manual= from URL so prepareListFormMapForRequest
7196                            // does not re-inject Leaflet viewport bounds via the hasUrlMap branch.
7197                            try {
7198                                const _u = new URL(window.location.href);
7199                                if (_u.searchParams.has('map') || _u.searchParams.has('map_manual')) {
7200                                    _u.searchParams.delete('map');
7201                                    _u.searchParams.delete('map_manual');
7202                                    history.replaceState(null, '', _u.pathname + (_u.search || '') + (_u.hash || ''));
7203                                }
7204                            } catch(_eStripMapUrl) {}
7205                            // Exit bounds mode — we're now searching by city, not by viewport
7206                            try { window.__listBoundsMode = false; } catch(_e) {}
7207                            try { window.__landedWithMap = false; } catch(_e) {}
7208                            try {
7209                                if (window.MirelaSearch && typeof window.MirelaSearch.applyUserMovedMapList === 'function') {
7210                                    window.MirelaSearch.applyUserMovedMapList(false);
7211                                } else {
7212                                    window.__mirelaUserMovedMap = false;
7213                                }
7214                            } catch (_e) {}
7215                            try { window.__mapManualNext = false; } catch(_e) {}
7216                            // Force map to fit to response bounds (even if auto-search is on)
7217                            try { window.__listForceFitOnce = true; } catch(_e) {}
7218                            try { window.__listLocationSelectionFitNext = true; } catch(_e) {}
7219                            // Consume pending bounds (no longer needed, server will provide correct ones)
7220                            try {
7221                                window.__pendingLocationBounds = null;
7222                                window.__pendingLocationBoundsTs = 0;
7223                            } catch(_e) {}
7224                            try {
7225                                if (listRuntime && typeof listRu
7225ntime.makeRequest === 'function') {
7226                                    listRuntime.makeRequest({
7227                                        reason: 'search-autocomplete.locationSelection.cityOnly',
7228                                        requestMode: 'location',
7229                                        form: form,
7230                                        force: true,
7231                                        append: false,
7232                                        resetPaging: true,
7233                                        explicitLocation: true
7234                                    });
7235                                    return;
7236                                }
7237                            } catch(_e) {}
7238                        }
7239                    }
7240                }
7241            } catch(_e) {}
7242
7243            try {
7244                if (isListPageShell()) {
7245                    const form = document.querySelector('.js-search-form');
7246                    const listRuntime = window.MirelaSearch && window.MirelaSearch.listRuntime
7247                        ? window.MirelaSearch.listRuntime
7248                        : null;
7249                    if (form && listRuntime && typeof listRuntime.makeRequest === 'function') {
7250                        listRuntime.makeRequest({
7251                            reason: 'search-autocomplete.listFallbackRequest',
7252                            requestMode: 'location',
7253                            form: form,
7254                            force: true,
7255                            append: false,
7256                            resetPaging: true
7257                        });
7258                        return;
7259                    }
7260                }
7261            } catch (_eListFallback) {}
7262
7263            const btn = document.querySelector('.js-map-manual');
7264            if (btn) {
7265                try {
7266                    btn.dispatchEvent(new MouseEvent('click', { bubbles: false, cancelable: true }));
7267                } catch (err) {}
7268                return;
7269            }
7270
7271            // Fallback: no map/manual button present → build SEO route and redirect with current filters
7272            try {
7273                const form = document.querySelector('.js-search-form');
7274                if (!form) return;
7275
7276                const msFb = window.MirelaSearch;
7277                if (msFb && typeof msFb.tryOffersSearchServerListNavigationFromForm === 'function') {
7278                    const navFb = msFb.tryOffersSearchServerListNavigationFromForm(form);
7279                    if (navFb && navFb.handled) {
7280                        if (!navFb.sync && navFb.promise && typeof navFb.promise.catch === 'function') {
7281                            navFb.promise.catch(function () {});
7282                        }
7283                        return;
7284                    }
7285                }
7286            } catch(_e) {}
7287        }
7288
7289        // Public methods
7290        destroy() {
7291            if (this.isInitialized) {
7292                try { this.$inputs && this.$inputs.off(); } catch(_e) {}
7293                try { $(document).off('.searchAutocomplete'); } catch(_e) {}
7294                this.isInitialized = false;
7295            }
7296        }
7297
7298        refresh() {
7299            this.destroy();
7300            this.init();
7301        }
7302
7303        _removeHistorySuggestionItem(itemEl) {
7304            try { this._setActiveContextFromSuggestionEl(itemEl); } catch (_e) {}
7305            const $item = $(itemEl).closest('.suggestion-item');
7306            if (!$item.length) { return; }
7307            this._removeFromHistory($item);
7308            $item.remove();
7309            this._collapseSuggestionsIfEmpty();
7310        }
7311
7312        _collapseSuggestionsIfEmpty() {
7313            try {
7314                if (this.$suggestions && !this.$suggestions.find('.suggestion-item').length) {
7315                    this._collapseSuggestions(this.$suggestions);
7316                }
7317            } catch (_e) {}
7318        }
7319
7320        // Remove a single suggestion from history (localStorage)
7321        _removeFromHistory($item) {
7322            try {
7323                const raw = $item.attr('data-value') || '';
7324                if (!raw) return;
7325                let val = null;
7326                try { val = JSON.parse(raw.replace(/&#39;/g, "'")); } catch(_e) {}
7327                if (!val) return;
7328                const sig = acValueSig(val);
7329                [HISTORY.KEY_AUTOCOMPLETE_A, HISTORY.KEY_AUTOCOMPLETE].forEac
7329h(key => {
7330                    try {
7331                        const arr = HISTORY.get(key);
7332                        const filtered = arr.filter(el => {
7333                            try { return acValueSig(el && el.value ? el.value : el) !== sig; } catch(_e) { return true; }
7334                        });
7335                        if (filtered.length !== arr.length) {
7336                            window.localStorage.setItem(key, JSON.stringify(filtered));
7337                        }
7338                    } catch(_e) {}
7339                });
7340            } catch(_e) {}
7341        }
7342
7343        // Show recent-history panel (used by input click toggle). No history → no-op (do not clear AJAX suggestions).
7344        maybeShowRecentHistory() {
7345            try {
7346                const items = this.getRecentHistoryItems();
7347                if (items.length) {
7348                    this.displaySuggestions(items);
7349                    this.$suggestions.show();
7350                }
7351            } catch(_e) {}
7352        }
7353
7354        getRecentHistoryItems() {
7355            const limit = this.options.historyLimits.acA || 9;
7356            const uniq = HISTORY.recentAutocompleteItems(limit);
7357            if (!uniq.length) { return []; }
7358            // Map to suggestion items format expected by displaySuggestions
7359            return uniq.map(it => {
7360                const v = Object.assign({}, acNormalizeValue(it.value || {}));
7361                try { v.__fromHistory = true; } catch(_e) {}
7362                return { label: it.label || it.typeahead || it.suggestion || '', value: v };
7363            });
7364        }
7365    }
7366
7367    // Initialize when DOM is ready
7368    $(document).ready(function() {
7369        // Initialize for all search inputs.
7370        // On homepage, the instance runs in "suggestions-only" mode (see class init()) to avoid
7371        // double-binding selection handlers with `public/js/pages/home.js`.
7372        //
7373        // Include this file only once per page (via `search-bar-scripts` is enough). A second
7374        // <script> tag would hit this guard and do nothing — it does NOT merge different page
7375        // behaviors. Home vs agents vs list differ via the server page shell, not via
7376        // loading autocomplete twice.
7377        try {
7378            if (window.searchAutocomplete && window.searchAutocomplete.isInitialized === true) {
7379                return;
7380            }
7381            window.searchAutocomplete = new SearchAutocomplete();
7382        } catch (err) {
7383            // Ако autocomplete хвърли грешка, не чупи друг JS
7384        }
7385    });
7386
7387    // Export for global access
7388    window.SearchAutocomplete = SearchAutocomplete;
7389
7390})(jQuery);
7391
7392// ---- END: js/search-autocomplete.js ----
7393
7394// ---- START: js/components/last-list-url-service.js ----
7395/* Last-list context helper: SSR first, sessionStorage mirror second. */
7396(function (window, document) {
7397    'use strict';
7398
7399    var root = window.MirelaSearch = window.MirelaSearch || {};
7400    var api = root.lastListUrlService = root.lastListUrlService || {};
7401
7402    var DEAL_LIST_RE = /\/(?:prodazhbi|naemi|for-sale|for-rent|prodazha|arenda|verkauf|miete|kupuvachi|naemateli)(\/|\?|#|$)/i;
7403    var OFFER_LIKE_RE = /\/(?:prodazhbi|naemi|for-sale|for-rent|prodazha|arenda|verkauf|miete)\/[^/]+\/[^/]+-(\d{5,})$/i;
7404
7405    function trim(value) {
7406        return String(value == null ? '' : value).trim();
7407    }
7408
7409    function writeSessionMirror(urlValue, labelValue) {
7410        try {
7411            if (typeof sessionStorage === 'undefined' || !sessionStorage) return;
7412            sessionStorage.setItem('__mirela_last_list_url', String(urlValue || ''));
7413            sessionStorage.setItem('__mirela_last_list_label', String(labelValue || ''));
7414        } catch (_e) {}
7415    }
7416
7417    /**
7418     * Normalize to a relative path + query suitable for a last-list mirror.
7419     * @returns {string} '' if invalid
7420     */
7421    function normalizeLastListUrl(url, options) {
7422        var o = options || {};
7423        try {
7424            var s = trim(url);
7425            if (!s) return '';
7426            if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) {
7427                try {
7428                    var u = new URL(s);
7429                    s = (u.pathname || '') + (u.search || '');
7430                } catch (_e) {
7431                    return '';
7432                }
7433            }
7434            if (!s || s[0] !== '/') return '';
7435            if (o.skipListValidation) return s;
7436            return isPlausibleListRelativeUrl(s) ? s : '';
7437        } catch (_e2) {
7438            return '';
7439        }
7440    }
7441
7442    function isPlausibleListRelativeUrl(rel) {
7443        var s = trim(rel);
7444        if (!s || s[0] !== '/') return false;
7445        if (/(^|\/)offer\/\d+$/i.test(s)) return false;
7446        if (OFFER_LIKE_RE.test(s)) return false;
7447        try {
7448            var agentUtils = root.agentPathUtils || null;
7449            if (agentUtils && typeof agentUtils.isAgentListingPath === 'function' && agentUtils.isAgentListingPath(s)) {
7450                return true;
7451            }
7452        } catch (_eAgent) {}
7453        return DEAL_LIST_RE.test(s);
7454    }
7455
7456    function readLabelFromDom() {
7457        try {
7458            var inp =
7459                document.querySelector('.js-search-form .js-location-dd-input') ||
7460                document.querySelector('.js-search-form .js-search-input');
7461            var label = inp && inp.value != null ? trim(inp.value) : '';
7462            return label;
7463        } catch (_e) {
7464            return '';
7465        }
7466    }
7467
7468    /**
7469     * Drop one-shot query params from a list rel before persisting to session/offer-entry state.
7470     * Keeps ?page=N so list → offer → „Списък“ returns to the same results page.
7471     * Strips focus_offer_id (re-added on back navigation) and restore_scroll (one-shot).
7472     *
7473     * IMPORTANT: do not use URL/URLSearchParams to rewrite the query — they re-encode values and break
7474     * the `map=` param (pipes, bounds order, length). Strip named segments only, preserving bytes.
7475     */
7476    function normalizePersistentListRel(rel) {
7477        try {
7478            var s = trim(rel);
7479            if (!s) return s;
7480            if (/^https?:\/\//i.test(s)) {
7481                try {
7482                    var abs = new URL(s);
7483                    s = (abs.pathname || '') + (abs.search || '') + (abs.hash || '');
7484                } catch (_a) {
7485                    return rel;
7486                }
7487            }
7488            var q = s.indexOf('?');
7489            if (q === -1) return s;
7490            var path = s.slice(0, q);
7491            var rest = s.slice(q + 1);
7492            var hash = '';
7493            var h = rest.indexOf('#');
7494            if (h !== -1) {
7495                hash = rest.slice(h);
7496                rest = rest.slice(0, h);
7497            }
7498            if (!rest) return path + hash;
7499            var segments = rest.split('&');
7500            var kept = [];
7501            for (var i = 0; i < segments.length; i++) {
7502                var seg = segments[i];
7503                if (!seg) continue;
7504                var eq = seg.indexOf('=');
7505                var rawKey = eq === -1 ? seg : seg.slice(0, eq);
7506                var key = rawKey;
7507                try {
7508                    key = decodeURIComponent(String(rawKey).replace(/\+/g, ' '));
7509                } catch (_k) {}
7510                var br = key.indexOf('[');
7511                var base = br === -1 ? key : key.slice(0, br);
7512                if (base === 'focus_offer_id' || base === 'restore_scroll') {
7513                    continue;
7514                }
7515                kept.push(seg);
7516            }
7517            var qs = kept.join('&');
7518            return path + (qs ? '?' + qs : '') + hash;
7519        } catch (_e) {
7520            return rel;
7521        }
7522    }
7523
7524    /** @deprecated use normalizePersistentListRel */
7525    function stripFocusOfferIdFromRelativeUrl(rel) {
7526        return normalizePersistentListRel(rel);
7527    }
7528
7529    /**
7530     * After list scroll-to-offer: drop focus_offer_id from last-list and restore/entry session copies.
7531     */
7532    function stripFocusOfferIdFromAllPersistedListUrls() {
7533        try {
7534            var ctx = readLastListContext();
7535            if (ctx.url) {
7536                var nu = normalizePersistentListRel(ctx.url);
7537                if (nu !== ctx.url) {
7538                    writeLastListContext({
7539                        url: nu,
7540                        locationLabel: ctx.locationLabel,
7541                        skipListValidation: true
7542                    });
7543                }
7544            }
7545        } catch (_e0) {}
7546
7547        try {
7548            var rs = sessionStorage.getItem('__mirela_restore_list_scroll');
7549            if (rs) {
7550                var pr = JSON.parse(rs);
7551                if (pr && pr.listUrl) {
7552                    var nlr = normalizePersistentListRel(String(pr.listUrl));
7553                    if (nlr !== pr.listUrl) {
7554                        pr.listUrl = nlr;
7555                        sessionStorage.setItem('__mirela_restore_list_scroll', JSON.stringify(pr));
7556                    }
7557                }
7558            }
7559        } catch (_e2) {}
7560
7561        try {
7562            var oe = sessionStorage.getItem('mirela_offer_entry_state_v1');
7563            if (oe) {
7564                var po = JSON.parse(oe);
7565                if (po && po.listUrl) {
7566                    var nlo = normalizePersistentListRel(String(po.listUrl));
7567                    if (nlo !== po.listUrl) {
7568                        po.listUrl = nlo;
7569                        sessionStorage.setItem('mirela_offer_entry_state_v1', JSON.stringify(po));
7570                    }
7571                }
7572            }
7573        } catch (_e3) {}
7574
7575        try {
7576            var ssl = sessionStorage.getItem('__mirela_last_list_url');
7577            if (ssl) {
7578                var nls = normalizePersistentListRel(String(ssl));
7579                if (nls !== ssl) sessionStorage.setItem('__mirela_last_list_url', nls);
7580            }
7581        } catch (_e4) {}
7582    }
7583
7584    /** @param {{ url?: string, locationLabel?: string, readLabelFromDom?: boolean, skipListValidation?: boolean }} payload */
7585    function writeLastListContext(payload) {
7586        var p = payload || {};
7587        var rel = normalizeLastListUrl(p.url, { skipListValidation: !!p.skipListValidation });
7588        if (!rel) return false;
7589        rel = stripFocusOfferIdFromRelativeUrl(rel);
7590
7591        var label = trim(p.locationLabel);
7592        if (p.readLabelFromDom) {
7593            var domLabel = readLabelFromDom();
7594            if (domLabel) label = domLabel;
7595        }
7596
7597        writeSessionMirror(rel, label);
7598        return true;
7599    }
7600
7601    function readLastListContext() {
7602        var url = '';
7603        var locationLabel = '';
7604        try {
7605            var nav = window.__MIRELA_SSR_LAST_LIST_NAV__;
7606            if (nav && typeof nav === 'object' && nav.rel) {
7607                url = String(nav.rel || '');
7608                locationLabel = String(nav.label || '');
7609            }
7610        } catch (_eSsr) {}
7611        try {
7612            if (!url && typeof sessionStorage !== 'undefined' && sessionStorage) {
7613                url = String(sessionStorage.getItem('__mirela_last_list_url') || '');
7614                locationLabel = String(sessionStorage.getItem('__mirela_last_list_label') || '');
7615            }
7616        } catch (_eSs) {}
7617        return {
7618            url: url,
7619            locationLabel: locationLabel
7620        };
7621    }
7622
7623    function clearLastListUrlMirror() {
7624        try {
7625            if (typeof sessionStorage !== 'undefined' && sessionStorage) sessionStorage.removeItem('__mirela_last_list_url');
7626        } catch (_e0) {}
7627    }
7628
7629    function clearLocationLabelMirror() {
7630        try {
7631            if (typeof sessionStorage !== 'undefined' && sessionStorage) sessionStorage.removeItem('__mirela_last_list_label');
7632        } catch (_e0) {}
7633    }
7634
7635    /**
7636     * Drop offer list-back handoff markers when the user leaves preview for home/map shell.
7637     * Main session / last-list rel must not keep focus_offer_id for a later generic „Списък“.
7638     */
7639    function clearEphemeralOfferHandoff() {
7640        try {
7641            if (typeof sessionStorage !== 'undefined' && sessionStorage) {
7642                sessionStorage.removeItem('__mirela_focus_offer_id');
7643            }
7644        } catch (_e0) {}
7645
7646        try {
7647            if (typeof window.__mirelaClearOfferEntryState === 'function') {
7648                window.__mirelaClearOfferEntryState();
7649            }
7650        } catch (_e1) {}
7651
7652        stripFocusOfferIdFromAllPersistedListUrls();
7653
7654        try {
7655            var nav = window.MirelaSearch && window.MirelaSearch.nav;
7656            if (nav && typeof nav.setLastListContext === 'function') {
7657                var ctx = typeof nav.getLastListContext === 'function' ? nav.getLastListContext() : null;
7658                var listUrl = ctx && ctx.last_list_url ? String(ctx.last_list_url) : '';
7659                if (listUrl) {
7660                    nav.setLastListContext({
7661                        listUrl: listUrl,
7662                        offerId: '',
7663                        suppressFocusOfferId: true
7664                    });
7665                }
7666            }
7667        } catch (_e2) {}
7668
7669        try {
7670            var raw = sessionStorage.getItem('__mirela_restore_list_scroll');
7671            if (raw) {
7672                var pr = JSON.parse(raw);
7673                if (pr && typeof pr === 'object') {
7674                    delete pr.offerId;
7675                    pr.source = 'cleared';
7676                    sessionStorage.setItem('__mirela_restore_list_scroll', JSON.stringify(pr));
7677                }
7678            }
7679        } catch (_e3) {}
7680    }
7681
7682    api.normalizeLastListUrl = normalizeLastListUrl;
7683    api.isPlausibleListRelativeUrl = isPlausibleListRelativeUrl;
7684    api.writeLastListContext = writeLastListContext;
7685    api.readLastListContext = readLastListContext;
7686    api.clearLastListUrlMirror = clearLastListUrlMirror;
7687    api.clearLocationLabelMirror = clearLocationLabelMirror;
7688    api.normalizePersistentListRel = normalizePersistentListRel;
7689    api.stripFocusOfferIdFromRelativeUrl = stripFocusOfferIdFromRelativeUrl;
7690    api.stripFocusOfferIdFromAllPersistedListUrls = stripFocusOfferIdFromAllPersistedListUrls;
7691    api.clearEphemeralOfferHandoff = clearEphemeralOfferHandoff;
7692    window.__mirelaClearEphemeralOfferHandoff = clearEphemeralOfferHandoff;
7693})(window, document);
7694
7695// ---- END: js/components/last-list-url-service.js ----
7696
7697// ---- START: js/components/back-target-resolver.js ----
7698/* Single back-target resolver for list navigation (detail desktop + mobile). */
7699(function (window, document) {
7700    'use strict';
7701
7702    var root = window.MirelaSearch = window.MirelaSearch || {};
7703    var api = root.backTargetResolver = root.backTargetResolver || {};
7704
7705    var OFFER_ENTRY_SS_KEY = 'mirela_offer_entry_state_v1';
7706    var NAV_BACK_SS_KEY = '__mirela_nav_back_target';
7707    var SESSION_LIST_KEY = '__mirela_last_list_url';
7708    var OFFER_ENTRY_TTL_MS = 1000 * 60 * 60;
7709    var NAV_BACK_TTL_MS = 30000;
7710    var OFFER_PREVIEW_RESTORE_MAX_MS = 10 * 60 * 1000;
7711
7712    var DEAL_SEGMENT_PATTERN = '(?:prodazhbi|naemi|for-sale|for-rent|prodazha|arenda|verkauf|miete|kupuvachi|naemateli)';
7713
7714    function trim(value) {
7715        return String(value == null ? '' : value).trim();
7716    }
7717
7718    function safeJsonParse(s) {
7719        try {
7720            return JSON.parse(String(s || ''));
7721        } catch (_e) {
7722            return null;
7723        }
7724    }
7725
7726    function isLocaleSegment(seg) {
7727        var s = String(seg || '').toLowerCase();
7728        return s === 'en' || s === 'ru' || s === 'de' || s === 'bg';
7729    }
7730
7731    function detectLocaleFromPath(pathname) {
7732        try {
7733            if (root.routeUtils && typeof root.routeUtils.detectLocaleFromPath === 'function') {
7734                return String(root.routeUtils.detectLocaleFromPath(pathname || '') || 'bg').toLowerCase() || 'bg';
7735            }
7736        } catch (_e) {}
7737        try {
7738            var parts = String(pathname || '')
7739                .split('/')
7740                .filter(Boolean);
7741            var first = parts[0] || '';
7742            if (isLocaleSegment(first)) return first.toLowerCase();
7743        } catch (_e2) {}
7744        return 'bg';
7745    }
7746
7747    function normalizeRelUrl(urlLike) {
7748        try {
7749            if (!urlLike) return '';
7750            var s = trim(urlLike);
7751            if (!s) return '';
7752            if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) {
7753                try {
7754                    var u = new URL(s);
7755                    s = (u.pathname || '') + (u.search || '');
7756                } catch (_e) {
7757                    return '';
7758                }
7759            }
7760            if (!s || s[0] !== '/') return '';
7761            s = collapseMapOnlyRelToDealPath(s);
7762            return s;
7763        } catch (_e2) {
7764            return '';
7765        }
7766    }
7767
7768    function collapseMapOnlyRelToDealPath(rel) {
7769        try {
7770            var u = new URL(String(rel || ''), window.location.origin);
7771            var hasMap = trim(u.searchParams.get('map') || '') !== '' || trim(u.searchParams.get('map_bounds') || '') !== '';
7772            if (!hasMap) return rel;
7773            var locationKeys = [
7774                'city_id',
7775                'city_id[]',
7776                'district_id',
7777                'district_id[]',
7778                'district_group_id',
7779                'district_group_id[]',
7780                'street_id',
7781                'country_id',
7782                's'
7783            ];
7784            for (var i = 0; i < locationKeys.length; i++) {
7785                if (trim(u.searchParams.get(locationKeys[i]) || '') !== '') {
7786                    return rel;
7787                }
7788            }
7789            var parts = String(u.pathname || '').split('/').filter(
7789Boolean);
7790            if (!parts.length) return rel;
7791            var idx = isLocaleSegment(parts[0]) ? 1 : 0;
7792            var deal = parts[idx] || '';
7793            if (!new RegExp('^' + DEAL_SEGMENT_PATTERN + '$', 'i').test(deal)) {
7794                return rel;
7795            }
7796            var localePrefix = idx === 1 ? '/' + parts[0] : '';
7797            var collapsedPath = (localePrefix ? localePrefix + '/' : '/') + deal;
7798            if ((u.pathname || '') === collapsedPath) return rel;
7799            return collapsedPath + (u.search || '') + (u.hash || '');
7800        } catch (_e) {
7801            return rel;
7802        }
7803    }
7804
7805    function toAbsolute(rel) {
7806        var r = normalizeRelUrl(rel);
7807        if (!r) return '';
7808        try {
7809            return window.location.origin.replace(/\/$/, '') + r;
7810        } catch (_e) {
7811            return r;
7812        }
7813    }
7814
7815    /**
7816     * Locale handoff produced by nav.listBackAbsForCurrentUiLocale — must not treat like a normal list path.
7817     */
7818    function isLocaleChangeHandoffRel(rel) {
7819        try {
7820            var pathOnly = String(normalizeRelUrl(rel) || '').split('?')[0] || '';
7821            return /^\/change\/(?:en|ru|de|bg)(?:\/|$)/i.test(pathOnly);
7822        } catch (_e2) {
7823            return false;
7824        }
7825    }
7826
7827    function isListUrl(path) {
7828        try {
7829            var p = String(path || '');
7830            if (!p) return false;
7831            if (/(^|\/)offer\/\d+$/i.test(p)) return false;
7832            // Offer slugs end with -{offerId} where offerId is 5+ digits. Shorter trailing digits are often
7833            // list location slugs (e.g. ...-mladost-2-3-4), not offer IDs — keep parity with Blade / last-list-url.
7834            if (
7835                new RegExp('/' + DEAL_SEGMENT_PATTERN + '/[^/]+/[^/]+-\\d{5,}$', 'i').test(p)
7836            ) {
7837                return false;
7838            }
7839            return new RegExp('/' + DEAL_SEGMENT_PATTERN + '(/|\\?|#|$)', 'i').test(p);
7840        } catch (_e) {
7841            return false;
7842        }
7843    }
7844
7845    function isValidListRel(rel) {
7846        var n = normalizeRelUrl(rel);
7847        if (!n) return false;
7848        if (isLocaleChangeHandoffRel(n)) return true;
7849        try {
7850            var u = new URL(n, window.location.origin);
7851            return isListUrl(u.pathname || '');
7852        } catch (_e) {
7853            return false;
7854        }
7855    }
7856
7857    function isAgentListingPath(pathname) {
7858        try {
7859            var agentUtils = root.agentPathUtils || null;
7860            if (agentUtils && typeof agentUtils.isAgentListingPath === 'function') {
7861                return !!agentUtils.isAgentListingPath(pathname || '');
7862            }
7863        } catch (_e) {}
7864        return false;
7865    }
7866
7867    /** List-back targets: standard deal/demand lists plus agent SEO listing pages. */
7868    function isListBackTargetRel(rel) {
7869        var n = normalizeRelUrl(rel);
7870        if (!n) return false;
7871        if (isLocaleChangeHandoffRel(n)) return true;
7872        try {
7873            var u = new URL(n, window.location.origin);
7874            if (isAgentListingPath(u.pathname || '')) return true;
7875            return isListUrl(u.pathname || '');
7876        } catch (_e) {
7877            return false;
7878        }
7879    }
7880
7881    function getSvc() {
7882        try {
7883            return root.lastListUrlService || null;
7884        } catch (_e) {
7885            return null;
7886        }
7887    }
7888
7889    function svcValidRel(rel) {
7890        var svc = getSvc();
7891        if (svc && typeof svc.isPlausibleListRelativeUrl === 'function') {
7892            return !!svc.isPlausibleListRelativeUrl(normalizeRelUrl(rel));
7893        }
7894        return isListBackTargetRel(rel);
7895    }
7896
7897    function hasMapParam(rel) {
7898        try {
7899            var n = normalizeRelUrl(rel);
7900            if (!n) return false;
7901            var u = new URL(n, window.location.origin);
7902            return trim(u.searchParams.get('map') || '') !== '';
7903        } catch (_e) {
7904            return false;
7905        }
7906    }
7907
7908    /** Demands list (kupuvachi / naemateli), not a demand detail SEO URL. */
7909    function isDemandsListRel(relLike) {
7910        try {
7911            var n = normalizeRelUrl(relLike);
7912            if (!n) return false;
7913            var u = new URL(n, window.location.origin);
7914            var path = u.pathname || '';
7915            if (!/\/(kupuvachi|naemateli)(\/|$)/i.test(path)) {
7916                return false;
7917            }
7918            return isListUrl(path);
7919        } catch (_e) {
7920            return false;
7921        }
7922    }
7923
7924    /** User is on a demand detail page (slug-id under kupuvachi/naemateli), not the demands index. */
7925    function isDemandDetailBrowseContext() {
7926        try {
7927            var p = String(window.location.pathname || '');
7928            if (!/\/(kupuvachi|naemateli)\//i.test(p)) {
7929                return false;
7930            }
7931            return !isListUrl(p);
7932        } catch (_e2) {
7933            return false;
7934        }
7935    }
7936
7937    function listPageNumberFromRel(rel) {
7938        try {
7939            var u = new URL(normalizeRelUrl(rel), window.location.origin);
7940            var p = parseInt(String(u.searchParams.get('page') || '1'), 10);
7941            return isFinite(p) && p > 0 ? p : 1;
7942        } catch (_e) {
7943            return 1;
7944        }
7945    }
7946
7947    function hasComplexNamespace(rel) {
7948        try {
7949            var p = String(normalizeRelUrl(rel) || '').toLowerCase();
7950            return p.indexOf('/kompleks/') !== -1 || p.indexOf('/complex/') !== -1 || p.indexOf('/komplex/') !== -1;
7951        } catch (_e) {
7952            return false;
7953        }
7954    }
7955
7956    function hasStreetNamespace(rel) {
7957        try {
7958            var p = String(normalizeRelUrl(rel) || '').toLowerCase();
7959            return p.indexOf('/ulitsa/') !== -1 || p.indexOf('/street/') !== -1 || p.indexOf('/strasse/') !== -1;
7960        } catch (_e) {
7961            return false;
7962        }
7963    }
7964
7965    function preferRicherListUrl(currentUrl, candidateUrl) {
7966        var cur = normalizeRelUrl(currentUrl);
7967        var next = normalizeRelUrl(candidateUrl);
7968        if (!next) return cur;
7969
7970        var nextHandoff = isLocaleChangeHandoffRel(next);
7971        var curHandoff = isLocaleChangeHandoffRel(cur);
7972        var demandCtx = isDemandDetailBrowseContext();
7973
7974        if (!cur) {
7975            if (demandCtx && !nextHandoff && !isDemandsListRel(next)) {
7976                return '';
7977            }
7978            if (nextHandoff || isListBackTargetRel(next)) return next;
7979            return cur;
7980        }
7981
7982        // Patched /change/{locale}?to=… wins over plain list rel (correct UI locale + encoded inner URL).
7983        if (nextHandoff && !curHandoff) return next;
7984        if (curHandoff && !nextHandoff) return cur;
7985        if (nextHandoff && curHandoff) {
7986            return next.length >= cur.length ? next : cur;
7987        }
7988
7989        if (!isListBackTargetRel(next)) return cur;
7990        if (!isListBackTargetRel(cur)) return next;
7991
7992        // Preview/list-back: when one candidate points to an explicit complex path and the other
7993        // points to a street path, prefer the complex URL. Runtime canonical state can occasionally
7994        // rebuild `/ulitsa/...` from the current offer context, while the remembered/Blade list URL
7995        // still carries the authoritative `/kompleks/...` path from the list page.
7996        var curComplex = hasComplexNamespace(cur);
7997        var nextComplex = hasComplexNamespace(next);
7998        var curStreet = hasStreetNamespace(cur);
7999        var nextStreet = hasStreetNamespace(next);
8000        if (curComplex && nextStreet && !nextComplex) return cur;
8001        if (nextComplex && curStreet && !curComplex) return next;
8002
8003        if (demandCtx) {
8004            var curD = isDemandsListRel(cur);
8005            var nextD = isDemandsListRel(next);
8006            if (curD && !nextD) return cur;
8007            if (!curD && nextD) return next;
8008        }
8009
8010        var curHasMap = hasMapParam(cur);
8011        var nextHasMap = hasMapParam(next);
8012        if (nextHasMap && !curHasMap) return next;
8013        if (curHasMap && !nextHasMap) return cur;
8014
8015        var curPage = listPageNumberFromRel(cur);
8016        var nextPage = listPageNumberFromRel(next);
8017        if (nextPage > curPage) return next;
8018        if (curPage > nextPage) return cur;
8019
8020        try {
8021            var uCur = new URL(cur, window.location.origin);
8022            var uNext = new URL(next, window.location.origin);
8023            var curLen = String(uCur.search || '').length;
8024            var nextLen = String(uNext.search || '').length;
8025            if (nextLen > curLen) return next;
8026        } catch (_e) {}
8027
8028        return cur;
8029    }
8030
8031    function buildBackUrl(listRel, offerId) {
8032        try {
8033            if (root.nav && typeof root.nav.buildBackUrl === 'function') {
8034                return String(root.nav.buildBackUrl(listRel, offerId) || '');
8035            }
8036        } catch (_e) {}
8037        try {
8038            var rel0 = normalizeRelUrl(listRel);
8039            if (!rel0) return '';
8040            var oid = trim(offerId);
8041            if (!oid) return rel0;
8042            var u = new URL(rel0, window.location.origin);
8043            if (!trim(u.searchParams.get('focus_offer_id') || '')) {
8044                u.searchParams.set('focus_offer_id', oid);
8045            }
8046            return (u.pathname || '') + (u.search || '');
8047        } catch (_e2) {
8048            return normalizeRelUrl(listRel);
8049        }
8050    }
8051
8052    /**
8053     * Add focus_offer_id to the list URL, or inject it into the inner `to` of a /change/{locale}?to=… handoff.
8054     */
8055    function mergeFocusIntoListBackRel(rel, offerId) {
8056        var oid = trim(offerId);
8057        var n = normalizeRelUrl(rel);
8058        if (!n) return '';
8059        if (!oid) return n;
8060        if (isLocaleChangeHandoffRel(n)) {
8061            try {
8062                var u = new URL(n, window.location.origin);
8063                var rawTo = u.searchParams.get('to');
8064                if (!rawTo) {
8065                    return buildBackUrl(n, oid);
8066                }
8067                var innerPath = rawTo;
8068                try {
8069                    innerPath = decodeURIComponent(rawTo.replace(/\+/g, '%20'));
8070                } catch (_eDec) {}
8071                var pathForUrl = innerPath;
8072                if (pathForUrl.indexOf('http://') !== 0 && pathForUrl.indexOf('https://') !== 0) {
8073                    if (pathForUrl.charAt(0) !== '/') pathForUrl = '/' + pathForUrl;
8074                }
8075                var uInner = new URL(pathForUrl, window.location.origin);
8076                if (!trim(uInner.searchParams.get('focus_offer_id') || '')) {
8077                    uInner.searchParams.set('focus_offer_id', oid);
8078                }
8079                var newInner = (uInner.pathname || '') + (uInner.search || '');
8080                u.searchParams.set('to', newInner);
8081                return (u.pathname || '') + (u.search || '');
8082            } catch (_e) {
8083                return buildBackUrl(n, oid);
8084            }
8085        }
8086        return buildBackUrl(n, oid);
8087    }
8088
8089    function getCurrentOfferIdFromLocation() {
8090        try {
8091            var p = String(window.location.pathname || '');
8092            var m = p.match(/\/offer\/(\d+)(?:\/)?$/i);
8093            if (m && m[1]) return String(m[1]);
8094            var seg = p.split('/').filter(Boolean).pop() || '';
8095            m = seg.match(/-(\d+)$/);
8096            if (m && m[1]) return String(m[1]);
8097        } catch (_e) {}
8098        return '';
8099    }
8100
8101    /**
8102     * Session snapshot from the list → offer transition (not updated on in-offer “similar” clicks).
8103     */
8104    function readOfferEntryState() {
8105        try {
8106            if (typeof sessionStorage === 'undefined' || !sessionStorage) return null;
8107            var raw = sessionStorage.getItem(OFFER_ENTRY_SS_KEY);
8108            if (!raw) return null;
8109            var parsed = safeJsonParse(raw);
8110            if (!parsed || typeof parsed !== 'object') return null;
8111            var ts = parseInt(parsed.ts || '0', 10) || 0;
8112            if (!ts || Date.now() - ts > OFFER_ENTRY_TTL_MS) return null;
8113            var listUrl = normalizeRelUrl(parsed.listUrl || '');
8114            if (!listUrl || !isListBackTargetRel(listUrl)) return null;
8115            return { listUrl: listUrl, offerId: trim(parsed.offerId || '') };
8116        } catch (_e) {
8117            return null;
8118        }
8119    }
8120
8121    function readOfferEntryRel() {
8122        var st = readOfferEntryState();
8123        return st && st.listUrl ? st.listUrl : '';
8124    }
8125
8126    function readOfferEntryLock() {
8127        var st = readOfferEntryState();
8128        if (!st || !st.listUrl) return null;
8129        return {
8130            listUrl: normalizeRelUrl(st.listUrl),
8131            offerId: trim(st.offerId || '')
8132        };
8133    }
8134
8135    function readNavBackTargetRel() {
8136        try {
8137            if (typeof sessionStorage === 'undefined' || !sessionStorage) return '';
8138            var raw = sessionStorage.getItem(NAV_BACK_SS_KEY);
8139            if (!raw) return '';
8140            var parsed = safeJsonParse(raw);
8141            if (!parsed) return '';
8142            var ts = parseInt(parsed.ts || '0', 10) || 0;
8143            if (!ts || Date.now() - ts > NAV_BACK_TTL_MS) return '';
8144            var u = trim(parsed.url || '');
8145            if (!u) return '';
8146            return normalizeRelUrl(u);
8147        } catch (_e) {
8148            return '';
8149        }
8150    }
8151
8152    function readSsrLastListRel() {
8153        try {
8154            var nav = window.__MIRELA_SSR_LAST_LIST_NAV__;
8155            if (!nav || typeof nav !== 'object') return '';
8156            return normalizeRelUrl(nav.rel || '');
8157        } catch (_e) {
8158            return '';
8159        }
8160    }
8161
8162    function readLastListContextRel() {
8163        try {
8164            var svc = getSvc();
8165            if (svc && typeof svc.readLastListContext === 'function') {
8166                var ctx = svc.readLastListContext();
8167                var u = ctx && ctx.url ? trim(ctx.url) : '';
8168                return normalizeRelUrl(u);
8169            }
8170        } catch (_e) {}
8171        return '';
8172    }
8173
8174    function readSessionMirrorRel() {
8175        try {
8176            if (typeof sessionStorage === 'undefined' || !sessionStorage) return '';
8177            return normalizeRelUrl(sessionStorage.getItem(SESSION_LIST_KEY) || '');
8178        } catch (_e) {
8179            return '';
8180        }
8181    }
8182
8183    function readReferrerRel() {
8184        try {
8185            var ref = trim(document.referrer || '');
8186            if (!ref) return '';
8187            var u = new URL(ref, window.location.origin);
8188            if (u.origin !== window.location.origin) return '';
8189            return normalizeRelUrl((u.pathname || '') + (u.search || ''));
8190        } catch (_e) {
8191            return '';
8192        }
8193    }
8194
8195    function defaultFallbackListRel(pageLocale) {
8196        var loc = trim(pageLocale) || detectLocaleFromPath(window.location.pathname || '');
8197        var salesSlug =
8198            loc === 'en' ? 'for-sale' : loc === 'ru' ? 'prodazha' : 'prodazhbi';
8199        if (loc === 'bg') return '/' + salesSlug;
8200        return '/' + loc + '/' + salesSlug;
8201    }
8202
8203    /**
8204     * Strict waterfall; saved context vs referrer merged with preferRicherListUrl.
8205     * @param {{ pageLocale?: string, offerId?: string, forOfferPreview?: boolean }} options
8206     * @returns {{ rel: string, abs: string, source: string }}
8207     */
8208    function resolveBackTarget(options) {
8209        var o = options || {};
8210        var pageLocale = trim(o.pageLocale) || detectLocaleFromPath(window.location.pathname || '');
8211        var offerIdFromPage = trim(o.offerId) || getCurrentOfferIdFromLocation();
8212        var entrySt = readOfferEntryState();
8213        var focusOfferId =
8214            entrySt && trim(entrySt.offerId) ? trim(entrySt.offerId) : offerIdFromPage;
8215
8216        function withFocus(rel, source) {
8217            var r = normalizeRelUrl(rel);
8218            if (!r || !svcValidRel(r)) return null;
8219            if (!isListBackTargetRel(r)) return null;
8220            if (isDemandDetailBrowseContext() && !isDemandsListRel(r)) return null;
8221            var built = focusOfferId ? buildBackUrl(r, focusOfferId) : r;
8222            var b = normalizeRelUrl(built);
8223            if (!b || !isListBackTargetRel(b)) return null;
8224            return { rel: b, abs: toAbsolute(b), source: source };
8225        }
8226
8227        var fromEntry = readOfferEntryRel();
8228        if (fromEntry) {
8229            var a = withFocus(fromEntry, 'offer_entry');
8230            if (a) return a;
8231        }
8232
8233        var fromNavBack = readNavBackTargetRel();
8234        if (fromNavBack) {
8235            var b = withFocus(fromNavBack, 'nav_back_target');
8236            if (b) return b;
8237        }
8238
8239        var refRelEarly = readReferrerRel();
8240        if (refRelEarly) {
8241            try {
8242                var refPathEarly = new URL(refRelEarly, window.location.origin).pathname || '';
8243                if (isAgentListingPath(refPathEarly)) {
8244                    var agentRef = withFocus(refRelEarly, 'referrer_agent');
8245                    if (agentRef) return agentRef;
8246                }
8247            } catch (_eAgentRef) {}
8248        }
8249
8250        var fromSsrNav = readSsrLastListRel();
8251        if (fromSsrNav) {
8252            var s = withFocus(fromSsrNav, 'ssr_session_nav');
8253            if (s) return s;
8254        }
8255
8256        var contextRel = readLastListContextRel();
8257        var refRel = readReferrerRel();
8258        var merged = '';
8259        if (contextRel && refRel && isListBackTargetRel(contextRel) && isListBackTargetRel(refRel)) {
8260            merged = preferRicherListUrl(contextRel, refRel);
8261        } else if (contextRel && isListBackTargetRel(contextRel)) {
8262            merged = contextRel;
8263        } else if (refRel && isListBackTargetRel(refRel)) {
8264            merged = refRel;
8265        }
8266        if (merged) {
8267            var d = withFocus(merged, 'context_or_referrer');
8268            if (d) return d;
8269        }
8270
8271        var sessionRel = readSessionMirrorRel();
8272        if (sessionRel) {
8273            var e = withFocus(sessionRel, 'session_mirror');
8274            if (e) return e;
8275        }
8276
8277        var fb = defaultFallbackListRel(pageLocale);
8278        var f = withFocus(fb, 'fallback');
8279        if (f) return f;
8280        return { rel: '', abs: '', source: 'none' };
8281    }
8282
8283    function bladeToRel(bladeHref) {
8284        if (!bladeHref || bladeHref === '#') return '';
8285        try {
8286            if (bladeHref.indexOf('http://') === 0 || bladeHref.indexOf('https://') === 0) {
8287                var u = new URL(bladeHref);
8288                if (u.origin !== window.location.origin) return '';
8289                return normalizeRelUrl((u.pathname || '') + (u.search || ''));
8290            }
8291        } catch (_e) {}
8292        return normalizeRelUrl(bladeHref);
8293    }
8294
8295    /**
8296     * Fresh offer-preview list URL stamped before navigation (mobile / tests).
8297     */
8298    function readOfferPreviewRestoreListRel() {
8299        try {
8300            if (typeof sessionStorage === 'undefined' || !sessionStorage) return '';
8301            var raw = trim(sessionStorage.getItem('__mirela_restore_list_scroll') || '');
8302            if (!raw) return '';
8303            var parsed = safeJsonParse(raw);
8304            if (!parsed || typeof parsed !== 'object') return '';
8305            var ts = parseInt(String(parsed.ts || '0'), 10) || 0;
8306            if (!ts || Date.now() - ts > OFFER_PREVIEW_RESTORE_MAX_MS) return '';
8307            if (String(parsed.source || '').trim() !== 'offer_preview') return '';
8308            var rel = normalizeRelUrl(parsed.listUrl || '');
8309            if (!rel || !isValidListRel(rel)) return '';
8310            return rel;
8311        } catch (_e2) {
8312            return '';
8313        }
8314    }
8315
8316    function mergeNavContextListUrl(listUrl, offerId) {
8317        try {
8318            if (!root.nav || typeof root.nav.getLastListContext !== 'function') return listUrl;
8319            var ctx = root.nav.getLastListContext();
8320            var ctxUrl = ctx && ctx.last_list_url ? trim(ctx.last_list_url) : '';
8321            var ctxOid = ctx && ctx.last_offer_id != null ? String(ctx.last_offer_id).trim() : '';
8322            var oid = trim(offerId);
8323            var sameOffer = !oid || (ctxOid && ctxOid === oid);
8324            if (ctxUrl && sameOffer) {
8325                var exactListUrl = normalizeRelUrl(listUrl);
8326                var navCtxListUrl = normalizeRelUrl(ctxUrl);
8327                if (isDemandDetailBrowseContext()) {
8328                    if (isDemandsListRel(exactListUrl) && !isDemandsListRel(navCtxListUrl)) {
8329                        return exactListUrl;
8330                    }
8331                    if (!isDemandsListRel(exactListUrl) && isDemandsListRel(navCtxListUrl)) {
8332                        return navCtxListUrl;
8333                    }
8334                }
8335                if (
8336                    exactListUrl &&
8337                    navCtxListUrl &&
8338                    !hasMapParam(exactListUrl) &&
8339                    hasMapParam(navCtxListUrl)
8340                ) {
8341                    return exactListUrl;
8342                }
8343                return preferRicherListUrl(exactListUrl, navCtxListUrl);
8344            }
8345        } catch (_e) {}
8346        return listUrl;
8347    }
8348
8349    /**
8350     * Single entry point for desktop .js-list-back (subscribe) and mobile goToList().
8351     * @param {{ mode?: 'subscribe'|'mobile', offerId?: string, pageLocale?: string, listBackHrefOrDataUrl?: string, fallbackRel?: string }} options
8352     * @returns {string} pathname+search with focus_offer_id (and inner to= when handoff)
8353     */
8354    function composeListBackRel(options) {
8355        var o = options || {};
8356        var mode = trim(o.mode) || 'subscribe';
8357        var offerIdFromPage = trim(o.offerId) || getCurrentOfferIdFromLocation();
8358        var entryStForFocus = readOfferEntryState();
8359        var entryLock = readOfferEntryLock();
8360        var focusOfferId =
8361            entryStForFocus && trim(entryStForFocus.offerId)
8362                ? trim(entryStForFocus.offerId)
8363                : offerIdFromPage;
8364        var pageLocale = trim(o.pageLocale) || detectLocaleFromPath(window.location.pathname || '');
8365        var listUrl = null;
8366
8367        var exact = readOfferEntryRel();
8368        if (exact && isListBackTargetRel(normalizeRelUrl(exact))) {
8369            if (!isDemandDetailBrowseContext() || isDemandsListRel(exact)) {
8370                listUrl = normalizeRelUrl(exact);
8371            }
8372        }
8373
8374        if (mode === 'subscribe') {
8375            var hasExactEntryListUrl = !!(entryLock && trim(entryLock.listUrl));
8376            if (hasExactEntryListUrl) {
8377                listUrl = normalizeRelUrl(entryLock.listUrl);
8378                if (entryLock.offerId) {
8379                    focusOfferId = entryLock.offerId;
8380                }
8381            } else {
8382                listUrl = mergeNavContextListUrl(listUrl, focusOfferId);
8383                if (o.listBackHrefOrDataUrl) {
8384                    var d0 = bladeToRel(trim(o.listBackHrefOrDataUrl));
8385                    if (d0) listUrl = preferRicherListUrl(listUrl, d0);
8386                }
8387                var resS = resolveBackTarget({ offerId: offerIdFromPage, pageLocale: pageLocale });
8388                if (resS && resS.rel && trim(resS.rel)) {
8389                    listUrl = preferRicherListUrl(listUrl, resS.rel);
8390                }
8391            }
8392            if (!listUrl) {
8393                var ctxS = readLastListContextRel();
8394                if (ctxS && (!isDemandDetailBrowseContext() || isDemandsListRel(ctxS))) {
8395                    listUrl = normalizeRelUrl(ctxS);
8396                }
8397            }
8398        } else {
8399            // mobile: match legacy goToList — no early nav;
8399 preview / resolver short-circuit before context/page/referrer waterfall
8400            var hasLock = !!(listUrl && trim(listUrl));
8401            if (!hasLock) {
8402                var prev = readOfferPreviewRestoreListRel();
8403                if (prev) {
8404                    listUrl = prev;
8405                    hasLock = true;
8406                }
8407            }
8408            if (!hasLock) {
8409                var resM = resolveBackTarget({ offerId: offerIdFromPage, pageLocale: pageLocale });
8410                if (resM && resM.rel && trim(resM.rel)) {
8411                    listUrl = normalizeRelUrl(resM.rel);
8412                    hasLock = true;
8413                }
8414            }
8415            if (!hasLock) {
8416                listUrl = mergeNavContextListUrl(listUrl, focusOfferId);
8417                if (o.listBackHrefOrDataUrl) {
8418                    var d1 = bladeToRel(trim(o.listBackHrefOrDataUrl));
8419                    if (d1) listUrl = preferRicherListUrl(listUrl, d1);
8420                }
8421                var refM = readReferrerRel();
8422                if (refM && isListBackTargetRel(refM)) listUrl = preferRicherListUrl(listUrl, refM);
8423                var sessM = readSessionMirrorRel();
8424                if (sessM && isListBackTargetRel(sessM)) listUrl = preferRicherListUrl(listUrl, sessM);
8425                var ctxM = readLastListContextRel();
8426                if (ctxM && isListBackTargetRel(ctxM)) {
8427                    if (!isDemandDetailBrowseContext() || isDemandsListRel(ctxM)) {
8428                        listUrl = preferRicherListUrl(listUrl, ctxM);
8429                    }
8430                }
8431            }
8432        }
8433
8434        if (isDemandDetailBrowseContext() && listUrl && !isDemandsListRel(listUrl)) {
8435            var bladeSan = o.listBackHrefOrDataUrl ? bladeToRel(trim(o.listBackHrefOrDataUrl)) : '';
8436            if (isDemandsListRel(bladeSan)) {
8437                listUrl = bladeSan;
8438            } else {
8439                var exSan = readOfferEntryRel();
8440                if (isDemandsListRel(exSan)) {
8441                    listUrl = normalizeRelUrl(exSan);
8442                } else {
8443                    var refSan = readReferrerRel();
8444                    if (isDemandsListRel(refSan)) {
8445                        listUrl = refSan;
8446                    }
8447                }
8448            }
8449        }
8450
8451        if (!listUrl || !trim(listUrl)) {
8452            var fb = trim(o.fallbackRel);
8453            if (fb) listUrl = normalizeRelUrl(fb);
8454        }
8455
8456        if (o.suppressFocusOfferId) {
8457            return listUrl || '';
8458        }
8459
8460        if (!focusOfferId) return listUrl || '';
8461        return mergeFocusIntoListBackRel(listUrl || '', focusOfferId) || listUrl || '';
8462    }
8463
8464    /**
8465     * Patch .js-list-back anchors when runtime target is valid and richer than Blade.
8466     * After a header locale switch, Blade/session often has the localized list path + filters but
8467     * deliberately omits focus_offer_id; preferRicherListUrl() alone would keep that href and skip
8468     * patching — merge focus onto the chosen base URL so "Списък" still returns to the same offer.
8469     */
8470    function patchJsListBackAnchors() {
8471        try {
8472            var anchors = document.querySelectorAll('a.js-list-back');
8473            if (!anchors || !anchors.length) return;
8474
8475            var offerIdFromPage = getCurrentOfferIdFromLocation();
8476            var entryPatch = readOfferEntryState();
8477            var entryLock = readOfferEntryLock();
8478            var focusOfferId =
8479                entryPatch && trim(entryPatch.offerId) ? trim(entryPatch.offerId) : offerIdFromPage;
8480            var pageLocale = detectLocaleFromPath(window.location.pathname || '');
8481            var resolved = entryLock && entryLock.listUrl
8482                ? {
8483                    rel: normalizeRelUrl(entryLock.listUrl),
8484                    abs: toAbsolute(normalizeRelUrl(entryLock.listUrl)),
8485                    source: 'offer_entry'
8486                }
8487                : resolveBackTarget({
8488                    pageLocale: pageLocale,
8489                    offerId: offerIdFromPage,
8490                    forOfferPreview: true
8491                });
8492
8493            if (!resolved || !resolved.rel || !resolved.abs) return;
8494            if (entryLock && entryLock.offerId) {
8495                focusOfferId = entryLock.offerId;
8496            }
8497
8498            for (var i = 0; i < anchors.length; i++) {
8499                var a = anchors[i];
8500                var bladeHref = trim(a.getAttribute('data-list-url') || a.getAttribute('href') || '');
8501                var bladeRel = bladeToRel(bladeHref);
8502
8503                var mergedBase = normalizeRelUrl(resolved.rel);
8504                if (!(entryLock && entryLock.listUrl) && bladeRel && isListBackTargetRel(bladeRel)) {
8505                    // Keep the exact Blade list-back URL when it already points to an explicit complex path.
8506                    // Runtime state can occasionally rebuild the same target as `/ulitsa/...` from current preview
8507                    // state, but the anchor already carries the authoritative list URL from the list page.
8508                    if (hasComplexNamespace(bladeRel) && hasStreetNamespace(resolved.rel)) {
8509                        mergedBase = normalizeRelUrl(bladeRel);
8510                    } else {
8511                        mergedBase = preferRicherListUrl(bladeRel, resolved.rel);
8512                    }
8513                }
8514                if (!mergedBase || !isListBackTargetRel(normalizeRelUrl(mergedBase))) {
8515                    mergedBase = normalizeRelUrl(resolved.rel);
8516                }
8517
8518                var finalRel = focusOfferId
8519                    ? mergeFocusIntoListBackRel(mergedBase, focusOfferId)
8520                    : normalizeRelUrl(mergedBase);
8521                if (!finalRel || !trim(finalRel)) continue;
8522
8523                var finalAbs = toAbsolute(finalRel);
8524                try {
8525                    if (root.nav && typeof root.nav.listBackAbsForCurrentUiLocale === 'function') {
8526                        var wrapped = root.nav.listBackAbsForCurrentUiLocale(finalRel);
8527                        if (wrapped) finalAbs = wrapped;
8528                    }
8529                } catch (_eN) {}
8530
8531                var same = false;
8532                try {
8533                    var uCur = new URL(bladeRel || '/', window.location.origin);
8534                    var uNew = new URL(finalAbs);
8535                    same =
8536                        String(uCur.pathname || '') === String(uNew.pathname || '') &&
8537                        String(uCur.search || '') === String(uNew.search || '');
8538                } catch (_eEq) {}
8539                if (same) continue;
8540
8541                a.setAttribute('href', finalAbs);
8542                a.setAttribute('data-list-url', finalAbs);
8543                try {
8544                    a.classList.remove('js-list-back-pending');
8545                    a.removeAttribute('aria-hidden');
8546                    a.style.removeProperty('display');
8547                } catch (_eShow) {}
8548            }
8549        } catch (_e) {}
8550    }
8551
8552    api.detectLocaleFromPath = detectLocaleFromPath;
8553    api.normalizeRelUrl = normalizeRelUrl;
8554    api.preferRicherListUrl = preferRicherListUrl;
8555    api.isValidListRel = isValidListRel;
8556    api.isListBackTargetRel = isListBackTargetRel;
8557    api.isAgentListingPath = isAgentListingPath;
8558    api.isLocaleChangeHandoffRel = isLocaleChangeHandoffRel;
8559    api.mergeFocusIntoListBackRel = mergeFocusIntoListBackRel;
8560    api.bladeToRel = bladeToRel;
8561    api.resolveBackTarget = resolveBackTarget;
8562    api.composeListBackRel = composeListBackRel;
8563    api.patchJsListBackAnchors = patchJsListBackAnchors;
8564    api.getCurrentOfferIdFromLocation = getCurrentOfferIdFromLocation;
8565    api.isDemandsListRel = isDemandsListRel;
8566    api.isDemandDetailBrowseContext = isDemandDetailBrowseContext;
8567})(window, document);
8568
8569// ---- END: js/components/back-target-resolver.js ----
8570
8571// ---- START: js/components/search-last-list-url.js ----
8572/* Build/persist last list URL + location label from filter state as a server-owned root path + query. */
8573(function (window, document) {
8574    'use strict';
8575
8576    var root = window.MirelaSearch = window.MirelaSearch || {};
8577    var api = root.lastListUrl = root.lastListUrl || {};
8578    var route = root.routeUtils || {};
8579    var TRANSIENT_PERSISTENCE_FIELDS = {
8580        map_user: true,
8581        map_lat: true,
8582        map_lng: true,
8583        map_zoom: true,
8584        extras: true
8585    };
8586
8587    function getLocalePrefixForCurrentPath() {
8588        try {
8589            if (route && typeof route.getLocalePrefix === 'function') {
8590                return String(route.getLocalePrefix(window.location.pathname || '') || '');
8591            }
8592        } catch (_e) {}
8593        try {
8594            var htmlLang = (document.documentElement.getAttribute('lang') || 'bg').toLowerCase();
8595            var seg = (htmlLang.split('-')[0] || '').toLowerCase();
8596            return (!seg || seg === 'bg') ? '' : ('/' + seg);
8597        } catch (_e2) {
8598            return '';
8599        }
8600    }
8601
8602    function resolveBasePath(opts) {
8603        try {
8604            var o = opts || {};
8605            try {
8606                var b = (o.basePath == null) ? '' : String(o.basePath);
8607                if (b && b[0] === '/') return b;
8608            } catch (_e) {}
8609            try {
8610                var p = String(window.location.pathname || '');
8611                return (p && p[0] === '/') ? p : '';
8612            } catch (_e2) { return ''; }
8613        } catch (_e3) {
8614            return '';
8615        }
8616    }
8617
8618    function buildCanonicalPersistedState(st) {
8619        var copy = Object.assign({}, st || {});
8620        try {
8621            Object.keys(TRANSIENT_PERSISTENCE_FIELDS).forEach(function (key) {
8622                delete copy[key];
8623            });
8624        } catch (_e0) {}
8625        try {
8626            ['property_type', 'focus', 'construction_type', 'construction_stage', 'land_only'].forEach(function (key) {
8627                if (copy[key] == null) return;
8628                if (!Array.isArray(copy[key])) copy[key] = [copy[key]];
8629                copy[key] = copy[key].map(function (value) { return String(value == null ? '' : value).trim(); }).filter(Boolean);
8630            });
8631        } catch (_e) {}
8632        return copy;
8633    }
8634
8635    function buildDealRootPath(st) {
8636        var prefix = getLocalePrefixForCurrentPath();
8637        var deal = '/prodazhbi';
8638        var t = String((st && st.type) || '').trim();
8639        if (t === '2') deal = '/naemi';
8640        else if (t === '3') deal = '/kupuvachi';
8641        else if (t === '4') deal = '/naemateli';
8642        return prefix + deal;
8643    }
8644
8645    function appendStateValue(params, key, value) {
8646        if (value == null) return;
8647        if (Array.isArray(value)) {
8648            value.forEach(function (item) {
8649                var text = String(item == null ? '' : item).trim();
8650                if (!text || text === '0') return;
8651                params.append(/\[\]$/.test(key) ? key : (key + '[]'), text);
8652            });
8653            return;
8654        }
8655        var textValue = String(value).trim();
8656        if (!textValue || textValue === '0') return;
8657        params.append(key, textValue);
8658    }
8659
8660    function buildSyntheticLastListUrl(st, basePathOverride) {
8661        try {
8662            if (!st) return '';
8663            var pathOnly = buildDealRootPath(st);
8664            try {
8665                var explicitBase = String(basePathOverride || '').trim();
8666                if (explicitBase && explicitBase[0] === '/') {
8667                    pathOnly = explicitBase.split('?')[0] || pathOnly;
8668                }
8669            } catch (_eBase) {}
8670
8671            var persisted = buildCanonicalPersistedState(st);
8672            try {
8673                delete persisted.type;
8674            } catch (_eType) {}
8675
8676            var params = new URLSearchParams();
8677            Object.keys(persisted).forEach(function (key) {
8678                if (TRANSIENT_PERSISTENCE_FIELDS[key]) return;
8679                appendStateValue(params, key, persisted[key]);
8680            });
8681
8682            var query = params.toString();
8683            return pathOnly + (query ? ('?' + query) : '');
8684        } catch (_e7) { return ''; }
8685    }
8686
8687    function normalizeCurrentListRelFromWindow(basePathOverride) {
8688        try {
8689            var explicitBase = String(basePathOverride || '').trim();
8690            var current = String(window.location.pathname || '') + String(window.location.search || '');
8691            var rel = current && current[0] === '/' ? current : '';
8692            if (!rel && explicitBase && explicitBase[0] === '/') {
8693                rel = explicitBase;
8694            }
8695            if (!(rel && rel[0] === '/')) return '';
8696            var svc = root.lastListUrlService;
8697            if (svc && typeof svc.normalizePersistentListRel === 'function') {
8698                rel = svc.normalizePersistentListRel(rel);
8699            }
8700            return rel;
8701        } catch (_eNorm) {
8702            return '';
8703        }
8704    }
8705
8706    function persistLastListUrlAndLabel(st, opts) {
8707        try {
8708            var o = opts || {};
8709            var basePath = (o.basePath == null) ? '' : String(o.basePath);
8710            var rel = normalizeCurrentListRelFromWindow(basePath);
8711            if (!rel) {
8712                rel = buildSyntheticLastListUrl(st, basePath);
8713            }
8714            if (!(rel && rel[0] === '/')) return '';
8715
8716            var svc = root.lastListUrlService;
8717            if (svc && typeof svc.writeLastListContext === 'function') {
8718                var sLabel = (st && st.s != null) ? String(st.s).trim() : '';
8719                svc.writeLastListContext({
8720                    url: rel,
8721                    locationLabel: sLabel,
8722                    maxAge: o.maxAge
8723                });
8724                return rel;
8725            }
8726            return '';
8727        } catch (_e3) {
8728            return '';
8729        }
8730    }
8731
8732    api.resolveBasePath = resolveBasePath;
8733    api.buildSyntheticLastListUrl = buildSyntheticLastListUrl;
8734    api.normalizeCurrentListRelFromWindow = normalizeCurrentListRelFromWindow;
8735    api.persistLastListUrlAndLabel = persistLastListUrlAndLabel;
8736})(window, document);
8737
8738
8739// ---- END: js/components/search-last-list-url.js ----

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.