PageSourceSearch

https://thodupuzha.com/wp-content/plugins/listeo-core/assets/js/listeo.leaflet.js?ver=1789140579

js thodupuzha.com collected 2026-10-03 00:36:14 UTC 107,254 bytes, 2,660 lines download raw bytes

1/* ----------------- Start Document ----------------- */
2(function($){
3"use strict";
4
5$(document).ready(function(){
6
7  var infoBox_ratingType = 'star-rating';
8  var url;
9
10  /**
11   * The Google Maps API is loaded with loading=async, so google.maps may
12   * not exist yet at DOM ready. Runs cb once the API is usable: right away
13   * if present, via the listeoOnGoogleMaps queue (inline shim printed
14   * before the API tag) when available, otherwise with a short poll for
15   * setups that load the API by other means.
16   */
17  function whenGoogleMapsReady(cb) {
18    if (window.google && window.google.maps && window.google.maps.Map) { cb(); return; }
19    if (window.listeoOnGoogleMaps) { window.listeoOnGoogleMaps(cb); return; }
20    var tries = 0;
21    var timer = setInterval(function() {
22      if (window.google && window.google.maps && window.google.maps.Map) {
23        clearInterval(timer);
24        cb();
25      } else if (++tries >= 100) {
26        clearInterval(timer);
27      }
28    }, 100);
29  }
30
31  /**
32   * Validates and normalizes latitude and longitude coordinates
33   * @param {*} lat - Latitude value to validate
34   * @param {*} lng - Longitude value to validate
35   * @returns {object|null} - Object with {lat, lng} as numbers if valid, null otherwise
36   */
37  function isValidCoordinate(lat, lng) {
38    // Check if values are not null, undefined, empty string
39    if (lat == null || lng == null || lat === '' || lng === '') {
40      console.warn('Listeo Map: Invalid coordinates - null or empty', {lat: lat, lng: lng});
41      return null;
42    }
43
44    // Convert to numbers - handle both string and numeric input
45    var latNum = typeof lat === 'number' ? lat : parseFloat(String(lat).trim().replace(',', '.'));
46    var lngNum = typeof lng === 'number' ? lng : parseFloat(String(lng).trim().replace(',', '.'));
47
48    // Check if conversion resulted in valid numbers
49    if (isNaN(latNum) || isNaN(lngNum)) {
50      console.warn('Listeo Map: Invalid coordinates - not numeric', {lat: lat, lng: lng, parsed: {latNum: latNum, lngNum: lngNum}});
51      return null;
52    }
53
54    // Check if latitude is within valid range (-90 to 90)
55    if (latNum < -90 || latNum > 90) {
56      console.warn('Listeo Map: Invalid latitude - out of range (-90 to 90)', {lat: latNum});
57      return null;
58    }
59
60    // Check if longitude is within valid range (-180 to 180)
61    if (lngNum < -180 || lngNum > 180) {
62      console.warn('Listeo Map: Invalid longitude - out of range (-180 to 180)', {lng: lngNum});
63      return null;
64    }
65
66    // Check for suspicious "zero coordinates" that might indicate missing data
67    if (latNum === 0 && lngNum === 0) {
68      console.warn('Listeo Map: Suspicious coordinates - both zero (0,0)', {lat: lat, lng: lng});
69      return null;
70    }
71
72    // Return normalized numeric coordinates
73    return {
74      lat: latNum,
75      lng: lngNum
76    };
77  }
78
79  function getMarkers() {
80        var arrMarkers = [];
81        $('.listing-geo-data').each(function(index) {
82          var point_address;
83          if( $( this ).data('friendly-address') ){
84            point_address = $( this ).data('friendly-address');
85          } else {
86            point_address = $( this ).data('address');
87          }
88          
89          if( $( this ).is('a') ){
90            url = $(this).find('a').attr('href');
91          } else {
92            url = $(this).find('a').attr('href');
93          }
94
95          if( $( this ).data('longitude') ) {
96            // badges can be in listing-list-small-badges-container, or listing-small-list-badges-container or listing-small-badges-container
97            // add a check for each of them and use first on that is found
98            var badges;
99            if( $( this ).find('.listing-list-small-badges-container').length > 0 ) {
100              badges = $( this ).find('.listing-list-small-badges-container').html();
101            } else if( $( this ).find('.listing-small-list-badges-container').length > 0 ) {
102               badges = $( this ).find('.listing-small-list-badges-container').html();
103            } else {
104              badges = $( this ).find('.listing-small-badges-container').html();
105            }
106            if( !badges ) {
107              badges = "";
108            }
109
110            var rating;
111            if( $( this ).find('.listing-rating-nl').length > 0 ) {
112              rating = $( this ).find('.listing-rating-nl').html();
113            } else {
114              rating = $(this).data("rating");
115            }
116
117            arrMarkers.push([
118              locationData(
119                $(this).find("a").attr("href")
120                  ? $(this).find("a").attr("href")
121                  : $(this).attr("href"),
122                $(this).data("image"),
123                $(this).data("title"),
124                $(this).data("listing-type"),
125                $(this).data("classifieds-price"),
126                point_address,
127                rating,
128                $(this).data("reviews"),
129                badges
130              ),
131              $(this).data("longitude"),
132              $(this).data("latitude"),
133              1,
134              $(this).data("icon"),
135            ]);
136          }
137        });
138        
139        return arrMarkers;
140    }
141
142    function starsOutput(firstStar, secondStar, thirdStar, fourthStar, fifthStar) {
143      return(''+
144        '<span class="'+firstStar+'"></span>'+
145        '<span class="'+secondStar+'"></span>'+
146        '<span class="'+thirdStar+'"></span>'+
147        '<span class="'+fourthStar+'"></span>'+
148        '<span class="'+fifthStar+'"></span>');
149    }
150
151  function locationData(locationURL,locationImg,locationTitle,locationType, locationPrice, locationAddress, locationRating, locationRatingCounter, badges) {
152         
153      var output;
154      var output_top;
155      var output_bottom;
156      output_top= ''+
157            '<a href="'+ locationURL +'" class="leaflet-listing-img-container">'+
158            '<div class="listing-small-badges-container">' + badges + '</div>' +
159               '<img src="'+locationImg+'" alt="">'+
160               '<div class="leaflet-listing-item-content">'+
161                  '<h3>'+locationTitle+'</h3>'+
162                  '<span>'+locationAddress+'</span>'+
163               '</div>'+
164               
165
166            '</a>'+
167
168            '<div class="leaflet-listing-content">'+
169               '<div class="listing-title">';
170               // classifieds and any non-bookable listing type expose a static
171               // price via data-classifieds-price (empty for bookable types)
172               if(locationType == 'classifieds' || locationPrice) {
173                      // .data() returns the attribute entity-decoded; re-encode
174                      // before concatenating into popup HTML
175                      var safeLocationPrice = locationPrice ? $('<div>').text(locationPrice).html() : '';
176                      output_bottom = '<div class="classifieds-price-infobox">'+safeLocationPrice+'</div>'+
177                         '</div>'+
178                      '</div>';
179               } else {
180              
181                // check if locationRating is a number and greater than 0
182                 
183                    if (typeof locationRating === 'number' && locationRating > 0) {
184                 
185
186                // Rating Stars Output
187                
188                    var fiveStars = starsOutput('star','star','star','star','star');
189
190                    var fourHalfStars = starsOutput('star','star','star','star','star half');
191                    var fourStars = starsOutput('star','star','star','star','star empty');
192
193                    var threeHalfStars = starsOutput('star','star','star','star half','star empty');
194                    var threeStars = starsOutput('star','star','star','star empty','star empty');
195
196                    var twoHalfStars = starsOutput('star','star','star half','star empty','star empty');
197                    var twoStars = starsOutput('star','star','star empty','star empty','star empty');
198
199                    var oneHalfStar = starsOutput('star','star half','star empty','star empty','star empty');
200                    var oneStar = starsOutput('star','star empty','star empty','star empty','star empty');
201
202                // Rules
203                    var stars;
204                    if (locationRating >= 4.75) {
205                        stars = fiveStars;
206                    } else if (locationRating >= 4.25) {
207                        stars = fourHalfStars;
208                    } else if (locationRating >= 3.75) {
209                        stars = fourStars;
210                    } else if (locationRating >= 3.25) {
211                        stars = threeHalfStars;
212                    } else if (locationRating >= 2.75) {
213                        stars = threeStars;
214                    } else if (locationRating >= 2.25) {
215                        stars = twoHalfStars;
216                    } else if (locationRating >= 1.75) {
217                        stars = twoStars;
218                    } else if (locationRating >= 1.25) {
219                        stars = oneHalfStar;
220                    } else if (locationRating < 1.25) {
221                        stars = oneStar;
222                    }
223              
224
225                      output_bottom = '<div class="'+infoBox_ratingType+'" data-rating="'+locationRating+'"><div class="rating-counter">('+locationRatingCounter+' '+listeo_core.maps_reviews_text+')</div>'+stars+'</div>'+
226                         '</div>'+
227                      '</div>';
228                  }
229                  // check if it's html code
230                  else if (typeof locationRating === 'string' && locationRating.trim() !== '') {
231                    output_bottom =
232                      '<div class="listing-rating-nl">' +
233                      locationRating +
234                      "</div>" +
235                      "</div>" +
236                      "</div>";
237                  } else {
238                    output_bottom = '<div class="'+infoBox_ratingType+'"><span class="not-rated">'+listeo_core.maps_noreviews_text+'</span></div>'+
239                         '</div>'+
240                      '</div>';
241                  }
242               }
243            
244        output = output_top+output_bottom;
245       
246
247        return output;
248    }
249
250     window.L_DISABLE_3D = true 
251        
252       
253      // console.log(locations);
254      var group;
255      var marker;
256      var locations;
257      var markerArray = [];
258      
259      // User location marker and radius circle variables
260      var userLocationMarker = null;
261      var radiusCircle = null;
262      var userLocationLayer = L.layerGroup();
263      var userLocationFirstShow = true;
264      
265
266      var latlngStr = listeo_core.centerPoint.split(",",2);
267      var lat = parseFloat(latlngStr[0]);
268      var lng = parseFloat(latlngStr[1]);
269      var mapZoomAttr = $('div#map').attr('data-map-zoom');
270      var mapScrollAttr = $('div#map').attr('data-map-scroll');
271     
272      var zoomLevel = parseInt(mapZoomAttr);
273
274      var markers;
275      if ( $('div#map').hasClass('split-map') && !("ontouchstart" in document.documentElement)) {
276        var mapOptions = {
277          center: [lat,lng],
278          zoom: zoomLevel,
279          zoomControl: false,
280          gestureHandling: false,
281          tap: false
282       }
283      } else {
284        var mapOptions = {
285          center: [lat,lng],
286          zoom: zoomLevel,
287          zoomControl: false,
288          gestureHandling: false,
289          tap: false
290       }
291      }
292
293      // MarkerCluster needs a finite maxZoom before the async Google layer is added.
294      if (listeo_core.map_provider === "google") {
295        mapOptions.maxZoom = 18;
296      }
297     
298
299      var _map = document.querySelectorAll("div#map");
300      // if  querySelectorAll has found an element
301      
302
303      
304     if (_map.length > 0) {
305       window.map = L.map("map", mapOptions);
306
307       switch (listeo_core.map_provider) {
308         case "osm":
309           L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
310             attribution:
311               '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
312           }).addTo(map);
313           break;
314
315         case "google":
316           var googleMutantOptions = {
317             type: "roadmap", // valid values are 'roadmap', 'satellite', 'terrain' and 'hybrid'
318             maxZoom: 18,
319           };
320
321           // Add Map ID if configured (for Cloud Styled Maps)
322           if (listeo_core.google_maps_id && listeo_core.google_maps_id !== '') {
323             googleMutantOptions.mapId = listeo_core.google_maps_id;
324           }
325
326           // Deferred so googleMutant's internal 10s wait-for-API timeout
327           // can't throw when the async API loads slowly.
328           whenGoogleMapsReady(function() {
329             L.gridLayer.googleMutant(googleMutantOptions).addTo(map);
330           });
331
332           break;
333
334         case "mapbox":
335           var accessToken = listeo_core.mapbox_access_token;
336           var mapbox_style_url = listeo_core.mapbox_style_url;
337
338           if (listeo_core.mapbox_retina) {
339             L.tileLayer(mapbox_style_url + accessToken, {
340               attribution:
341                 " &copy;  <a href='https://www.mapbox.com/about/maps/'>Mapbox</a> &copy;  <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> <strong><a href='https://www.mapbox.com/map-feedback/' target='_blank'>Improve this map</a></strong>",
342               maxZoom: 18,
343               zoomOffset: -1,
344               //detectRetina: true,
345               tileSize: 512,
346             }).addTo(map);
347           } else {
348             L.tileLayer(mapbox_style_url + accessToken, {
349               attribution:
350                 " &copy;  <a href='https://www.mapbox.com/about/maps/'>Mapbox</a> &copy;  <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> <strong><a href='https://www.mapbox.com/map-feedback/' target='_blank'>Improve this map</a></strong>",
351               maxZoom: 18,
352               //detectRetina: true,
353               id: "mapbox.streets",
354             }).addTo(map);
355           }
356
357           break;
358
359         case "bing":
360           var options = {
361             bingMapsKey: listeo_core.bing_maps_key,
362             imagerySet: "RoadOnDemand",
363           };
364
365           L.tileLayer.bing(options).addTo(map);
366           break;
367
368         case "thunderforest":
369           var tileUrl =
370               "https://tile.thunderforest.com/cycle/{z}/{x}/{y}{r}.png?apikey=" +
371               listeo_core.thunderforest_api_key,
372             layer = new L.TileLayer(tileUrl, { maxZoom: 18 });
373           map.addLayer(layer);
374           break;
375
376         case "here":
377           L.tileLayer
378             .here({
379               appId: listeo_core.here_app_id,
380               appCode: listeo_core.here_app_code,
381             })
382             .addTo(map);
383           break;
384       }
385
386       // Check if scroll should be enabled based on data attribute
387       var scrollEnabled = false;
388       if (typeof mapScrollAttr !== typeof undefined && mapScrollAttr !== false) {
389         scrollEnabled = (mapScrollAttr === 'true' || mapScrollAttr === '1' || mapScrollAttr === 1);
390       }
391       
392       // Enable or disable scroll based on the attribute
393       if (!scrollEnabled) {
394         map.scrollWheelZoom.disable();
395       }
396       
397       // Add zoom controls
398       var zoomOptions = {
399         zoomInText: '<i class="fa fa-plus" aria-hidden="true"></i>',
400         zoomOutText: '<i class="fa fa-minus" aria-hidden="true"></i>',
401       };
402       var zoom = L.control.zoom(zoomOptions);
403       zoom.addTo(map);
404
405       // Modify where the map is initialized (around line 472)
406       if (window.map) {
407         // Add user location layer to map
408         window.map.addLayer(userLocationLayer);
409         
410         // Add flag to track if map was moved by user
411         var userMovedMap = false;
412         var initialLoadComplete = false;
413         var ignoreMapEvents = false;
414         // Programmatic auto-location movement (geolocate button flyTo) is not
415         // a visitor map move and must not arm the map-bounds search.
416         var autoLocationMapMove = false;
417         var autoLocationFailsafeTimer = null;
418
419         // Set initial load complete after a short delay
420         setTimeout(function() {
421           initialLoadComplete = true;
422         }, 5000);
423
424         // Add movestart event to track user interaction
425         window.map.on("movestart", function(e) {
426           // Only set userMovedMap if initial load is complete and event is triggered by user action
427           if(initialLoadComplete && !!e.originalEvent) {
428             userMovedMap = true;
429           }
430         });
431
432         // NEW: Add event listener for dragstart to detect user initiated map movement
433         window.map.on('dragstart', function(e) {
434           if(ignoreMapEvents || autoLocationMapMove) return;
435           userMovedMap = true;
436         });
437
438         // NEW: Add event listener for zoomstart to detect user initiated zooming
439         // Leaflet also emits zoomstart for flyTo()/fitBounds(), hence the
440         // autoLocationMapMove guard. Leaflet 1.1.1 never passes originalEvent
441         // to dragstart/zoomstart, so the flag is the only reliable signal.
442         window.map.on('zoomstart', function(e) {
443           if(ignoreMapEvents || autoLocationMapMove) return;
444           userMovedMap = true;
445         });
446
447         // Add zoomend event to detect when user has finished zooming
448         window.map.on('zoomend', function(e) {
449           if(ignoreMapEvents) return;
450           if(!!e.originalEvent) {
451             userMovedMap = true;
452             userLocationFirstShow = false; // User has interacted, no more auto-fitting
453           }
454         });
455
456         // Add moveend event to detect when user has finished panning
457         window.map.on('moveend', function(e) {
458           if (autoLocationMapMove) {
459             // Consume the programmatic geolocate flyTo: clear the flags and
460             // any map-bounds inputs a stray earlier interaction injected.
461             clearTimeout(autoLocationFailsafeTimer);
462             userMovedMap = false;
463             autoLocationMapMove = false;
464             $('#listeo_core-search-form')
465               .find('.map-bounds, input[name="search_by_map_move"]')
466               .remove();
467             setTimeout(function () { ignoreMapEvents = false; }, 100);
468             return;
469           }
470           if(ignoreMapEvents) return;
471           if(!!e.originalEvent) {
472             userMovedMap = true;
473             userLocationFirstShow = false; // User has interacted, no more auto-fitting
474           }
475         });
476
477         // Add moveend event with debounce
478         window.map.on(
479           "moveend",
480           debounce(function () {
481             if (!initialLoadComplete) {
482               return;
483             }
484
485             if (userMovedMap && listeo_core.map_bounds_search == "on") {
486               updateListingsForMapBounds(map, true);
487               userMovedMap = false;
488             }
489           }, 300)
490         );
491       }
492     } 
493
494      
495      function debounce(func, wait) {
496        var timeout;
497        return function () {
498          var context = this,
499            args = arguments;
500          clearTimeout(timeout);
501          timeout = setTimeout(function () {
502            func.apply(context, args);
503          }, wait);
504        };
505      }
506      
507    // Function to add/update user location marker and radius circle
508    function updateUserLocationOnMap(lat, lng, radius, locationText) {
509      if (!window.map || !lat || !lng || !radius) {
510        return;
511      }
512
513      // Validate and normalize coordinates before creating user location marker
514      var validCoords = isValidCoordinate(lat, lng);
515      if (!validCoords) {
516        return;
517      }
518
519      try {
520        // Clear existing user location elements
521        userLocationLayer.clearLayers();
522
523        // Create user location marker with distinct styling
524        var userLocationIcon = L.divIcon({
525          iconAnchor: [7.5, 15],
526          popupAnchor: [0, -15],
527          className: 'listeo-user-location-marker',
528          html: '<div class="user-location-container">' +
529                '<div class="user-location-marker">' +
530                '</div>' +
531                '</div>'
532        });
533
534        userLocationMarker = L.marker([validCoords.lat, validCoords.lng], {
535          icon: userLocationIcon,
536          zIndexOffset: 1000 // Ensure it appears above other markers
537        });
538
539        // Create radius circle
540        var radiusUnit = listeo_core.radius_unit || 'km';
541        var radiusInMeters = radius * (radiusUnit === 'km' ? 1000 : 1609.34); // Convert to meters
542        radiusCircle = L.circle([validCoords.lat, validCoords.lng], {
543          radius: radiusInMeters,
544          fillColor: '#007cba',
545          fillOpacity: 0.1,
546          color: '#007cba',
547          weight: 2,
548          opacity: 0.8,
549          dashArray: '5, 10'
550        });
551
552        // Add to user location layer
553        userLocationLayer.addLayer(userLocationMarker);
554        userLocationLayer.addLayer(radiusCircle);
555
556        // Fit map to include radius circle only on first show and if autofit is enabled
557        if (listeo_core.maps_autofit == "on" && userLocationFirstShow && !userMovedMap) {
558          var bounds = radiusCircle.getBounds();
559          // Extend bounds to include existing markers if any
560          if (markerArray.length > 0) {
561            var listingsBounds = L.featureGroup(markerArray).getBounds();
562            bounds.extend(listingsBounds);
563          }
564          window.map.fitBounds(bounds, { padding: [20, 20] });
565          userLocationFirstShow = false; // Mark that we've done the initial auto-fit
566        }
567      } catch (error) {
568        // Clear any partial elements that may have been created
569        userLocationLayer.clearLayers();
570      }
571    }
572    
573    var allResultsManifest = null;
574    var allResultsManifestLoading = false;
575    var allResultsManifestFailed = false;
576    var allResultsInitialRender = true;
577    var allResultsManifestRendered = false;
578    var activeManifestSubset = null;
579    var activeManifestFilterSignature = '';
580    var clusterNavigationMove = false;
581    var manifestMapEventsBound = false;
582    var manifestMapRenderTimer = null;
583    var manifestInitialRenderTimer = null;
584    var manifestInitialRenderAttempts = 0;
585    var manifestInitialRenderMap = null;
586    var manifestInitialResizeObserver = null;
587    var manifestInitialVisibilityHandler = null;
588    var manifestInitialWatchersBound = false;
589    var clusterPrefetchTimer = null;
590    // Tier 1: small pre-clustered overview (zoom 0..N) painted immediately;
591    // tier 2: the full manifest, fetched at idle after the overview is on screen.
592    var overviewManifest = null;
593    var overviewManifestLoading = false;
594    var overviewManifestFailed = false;
595    var fullManifestLoadScheduled = false;
596    var fullManifestIntentArmed = false;
597
598    function stopAllResultsManifestInitialWatchers() {
599      clearTimeout(manifestInitialRenderTimer);
600      if (manifestInitialResizeObserver) {
601        manifestInitialResizeObserver.disconnect();
602        manifestInitialResizeObserver = null;
603      }
604      $(window).off('.listeoManifestInitial');
605      if (manifestInitialVisibilityHandler) {
606        document.removeEventListener('visibilitychange', manifestInitialVisibilityHandler);
607        manifestInitialVisibilityHandler = null;
608      }
609      manifestInitialWatchersBound = false;
610    }
611
612    function watchAllResultsManifestInitialRender(map) {
613      if (manifestInitialWatchersBound || !map) return;
614      manifestInitialWatchersBound = true;
615
616      var retryRender = function() {
617        if (!allResultsManifestRendered) {
618          renderLoadedAllResultsManifest(window.map || map);
619        }
620      };
621
622      $(window).on(
623        'load.listeoManifestInitial pageshow.listeoManifestInitial resize.listeoManifestInitial',
624        retryRender
625      );
626
627      manifestInitialVisibilityHandler = function() {
628        if (!document.hidden) retryRender();
629      };
630      document.addEventListener('visibilitychange', manifestInitialVisibilityHandler);
631
632      if (window.ResizeObserver && typeof map.getContainer === 'function') {
633        var container = map.getContainer();
634        if (container) {
635          manifestInitialResizeObserver = new ResizeObserver(function(entries) {
636            var rect = entries[0] && entries[0].contentRect;
637            if (rect && rect.width > 0 && rect.height > 0) retryRender();
638          });
639          manifestInitialResizeObserver.observe(container);
640        }
641      }
642    }
643
644    function getBoundsPayload(bounds) {
645      var ne = bounds.getNorthEast();
646      var sw = bounds.getSouthWest();
647      return {
648        ne_lat: ne.lat,
649        ne_lng: ne.lng,
650        sw_lat: sw.lat,
651        sw_lng: sw.lng
652      };
653    }
654
655    function updateListingsForMapBounds(map, triggerResults, clusterCacheToken) {
656      var payload = getBoundsPayload(map.getBounds());
657      var searchForm = $('#listeo_core-search-form');
658      searchForm.find('.map-bounds, input[name="search_by_map_move"]').remove();
659      Object.keys(payload).forEach(function(name) {
660        searchForm.append(
661          '<input type="hidden" class="map-bounds" name="map_bounds[' + name + ']" value="' + payload[name] + '">'
662        );
663      });
664      searchForm.append('<input type="hidden" name="search_by_map_move" value="true">');
665      if (triggerResults) {
666        $('div#listeo-listings-container').triggerHandler('update_results', [
667          1,
668          false,
669          false,
670          null,
671          clusterCacheToken || ''
672        ]);
673      }
674    }
675
676    function getClusterNavigation(map, group) {
677      var bounds = L.latLngBounds(
678        [group.minLat, group.minLng],
679        [group.maxLat, group.maxLng]
680      );
681      var center = bounds.getCenter();
682      var targetZoom = Math.min(map.getZoom() + 2, 18);
683      var useSetView = bounds.getNorthEast().distanceTo(bounds.getSouthWest
683()) < 30;
684
685      if (!useSetView && typeof map._getBoundsCenterZoom === 'function') {
686        var targetView = map._getBoundsCenterZoom(bounds, {
687          padding: [35, 35],
688          maxZoom: targetZoom
689        });
690        center = targetView.center;
691        targetZoom = targetView.zoom;
692      }
693
694      return {
695        bounds: bounds,
696        center: center,
697        zoom: targetZoom,
698        useSetView: useSetView
699      };
700    }
701
702    function getViewportBoundsForView(map, center, zoom) {
703      var size = map.getSize();
704      var halfWidth = size.x / 2;
705      var halfHeight = size.y / 2;
706      var centerPoint = map.project(center, zoom);
707      var northEast = map.unproject(L.point(centerPoint.x + halfWidth, centerPoint.y - halfHeight), zoom);
708      var southWest = map.unproject(L.point(centerPoint.x - halfWidth, centerPoint.y + halfHeight), zoom);
709      return L.latLngBounds(southWest, northEast);
710    }
711
712    function getClusterCacheToken(map, group) {
713      return [
714        map.getZoom(),
715        group.count,
716        group.minLat.toFixed(5),
717        group.maxLat.toFixed(5),
718        group.minLng.toFixed(5),
719        group.maxLng.toFixed(5)
720      ].join(':');
721    }
722
723    function prefetchClusterListings(map, group, clusterCacheToken) {
724      var navigation = getClusterNavigation(map, group);
725      var predictedBounds = getViewportBoundsForView(map, navigation.center, navigation.zoom);
726      $('div#listeo-listings-container').triggerHandler('prefetch_results', [
727        1,
728        false,
729        false,
730        getBoundsPayload(predictedBounds),
731        clusterCacheToken
732      ]);
733    }
734
735    function usesAllResultsManifest() {
736      return listeo_core.map_marker_source === 'all_results' &&
737        !!listeo_core.map_all_results_manifest_url &&
738        !allResultsManifestFailed;
739    }
740
741    function getOverviewManifestUrl() {
742      if (listeo_core.map_overview_manifest_url) {
743        return String(listeo_core.map_overview_manifest_url);
744      }
745      var url = String(listeo_core.map_all_results_manifest_url || '');
746      return url.indexOf('listing-points-') === -1 ? '' : url.replace('listing-points-', 'listing-overview-');
747    }
748
749    function usesOverviewManifest() {
750      return usesAllResultsManifest() && !overviewManifestFailed && !!getOverviewManifestUrl();
751    }
752
753    function scheduleFullManifestLoad(map) {
754      if (fullManifestLoadScheduled) return;
755      fullManifestLoadScheduled = true;
756      var start = function() {
757        ensureAllResultsManifest(window.map || map);
758      };
759      if (typeof window.requestIdleCallback === 'function') {
760        window.requestIdleCallback(function() { start(); }, { timeout: 1500 });
761      } else {
762        setTimeout(start, 300);
763      }
764    }
765
766    // Tier 2 is fetched only after a real person shows up: any trusted input
767    // anywhere on the page (synthetic audits like GTmetrix/Lighthouse never send
768    // one), or, under Save-Data, only a direct map/filter intent. Listeners are
769    // passive, fire once and are removed afterwards.
770    function armFullManifestLoadOnIntent(map) {
771      if (fullManifestIntentArmed || fullManifestLoadScheduled) return;
772      fullManifestIntentArmed = true;
773
774      var container = map && typeof map.getContainer === 'function' ? map.getContainer() : null;
775      var form = document.getElementById('listeo_core-search-form');
776      var saveData = !!(navigator.connection && navigator.connection.saveData);
777      var cleanups = [];
778      var fire = function(event) {
779        if (event && event.isTrusted === false) return;
780        cleanups.forEach(function(cleanup) { cleanup(); });
781        cleanups = [];
782        scheduleFullManifestLoad(map);
783      };
784      var listen = function(target, events, options) {
785        if (!target) return;
786        events.forEach(function(name) {
787          target.addEventListener(name, fire, options);
788          cleanups.push(function() { target.removeEventListener(name, fire, options); });
789        });
790      };
791
792      // Map intent: pointer, touch, wheel or keyboard on the map (covers drag,
793      // pinch, wheel zoom and the zoom buttons).
794      listen(container, ['mouseenter', 'pointerdown', 'touchstart', 'wheel', 'keydown'], { passive: true });
795      // Filter intent: focusing or changing anything in the search form.
796      listen(form, ['focusin', 'change', 'input'], { passive: true });
797      // Human presence: first input anywhere (capture also catches inner scrolls).
798      if (!saveData) {
799        listen(document, ['pointermove', 'mousemove', 'touchstart', 'keydown', 'wheel', 'scroll'], { passive: true, capture: true });
800      }
801    }
802
803    function ensureOverviewManifest(map) {
804      if (!usesOverviewManifest()) {
805        ensureAllResultsManifest(map);
806        return;
807      }
808      if (overviewManifest || overviewManifestLoading) {
809        return;
810      }
811      overviewManifestLoading = true;
812      $.ajax({
813        url: getOverviewManifestUrl(),
814        dataType: 'json',
815        cache: true
816      }).done(function(data) {
817        if (!data || !data.zooms || typeof data.zooms !== 'object') {
818          overviewManifestFailed = true;
819          return;
820        }
821        overviewManifest = data;
822        renderOverviewManifestMap(window.map || map);
823      }).fail(function() {
824        overviewManifestFailed = true;
825      }).always(function() {
826        overviewManifestLoading = false;
827        var manifestMap = window.map || map;
828        if (overviewManifestFailed) {
829          ensureAllResultsManifest(manifestMap);
830          return;
831        }
832        // Pages with an active, manifest-supported filter (taxonomy archives,
833        // preset searches) can't be painted from the unfiltered overview, so
834        // page markers are showing - fetch the full file right away to avoid
835        // the marker swap on first interaction. Unsupported filters keep the
836        // lazy path: they fall back to server markers anyway.
837        var filterState = getAllResultsFilterState(manifestMap);
838        if (filterState && !isAllResultsFilterEmpty(manifestMap)) {
839          scheduleFullManifestLoad(manifestMap);
840        } else {
841          armFullManifestLoadOnIntent(manifestMap);
842        }
843      });
844    }
845
846    function isAllResultsFilterEmpty(map) {
847      var state = getAllResultsFilterState(map);
848      return !!state && !state.categories.length && !state.regions.length && !state.types.length &&
849        !state.features.length && !state.rating && !state.price && !state.keyword && !state.location;
850    }
851
852    function createManifestClusterIcon(group) {
853      var sizeClass = group.count < 100 ? 'marker-cluster-small' : (group.count < 1000 ? 'marker-cluster-medium' : 'marker-cluster-large');
854      var roleClass = group.manifestRole ? ' listeo-manifest-' + group.manifestRole + '-cluster' : '';
855      return L.divIcon({
856        html: '<div><span>' + group.count.toLocaleString() + '</span></div>',
857        className: 'marker-cluster ' + sizeClass + roleClass,
858        iconSize: L.point(50, 50)
859      });
860    }
861
862    // Both manifest files carry a shared table of marker icons (one entry per
863    // distinct category icon) and every point stores its index in it, so pins
864    // match the ones the server renders into data-icon on listing cards.
865    // Files written before the icon table existed resolve to the default pin.
866    function getManifestIconHtml(source, index) {
867      var icons = source && Array.isArray(source.icons) ? source.icons : null;
868      var position = parseInt(index, 10);
869      if (icons && isFinite(position) && position >= 0 && position < icons.length && icons[position]) {
870        return String(icons[position]);
871      }
872      return '<i class="sl sl-icon-location"></i>';
873    }
874
875    function createManifestPointIcon(group, iconHtml) {
876      var pointRoleClass = group.manifestRole ? ' listeo-manifest-' + group.manifestRole + '-marker' : '';
877      var icon = iconHtml || '<i class="sl sl-icon-location"></i>';
878      return L.divIcon({
879        iconAnchor: [20, 51],
880        popupAnchor: [0, -51],
881        className: 'listeo-marker-icon' + pointRoleClass,
882        html: '<div class="marker-container"><div class="marker-card"><div class="front face">' + icon + '</div><div class="back face">' + icon + '</div><div class="marker-arrow"></div></div></div>'
883      });
884    }
885
886    // Renders the pre-clustered overview for the current zoom. Returns true when a
887    // layer was put on the map. Only used until the full manifest has rendered and
888    // only with an empty filter state (filters need per-point data).
889    function renderOverviewManifestMap(map) {
890      map = map || window.map;
891      if (!map || !map._loaded || !overviewManifest || !usesOverviewManifest() || allResultsManifestRendered) {
892        return false;
893      }
894      if (!isAllResultsFilterEmpty(map)) {
895        return false;
896      }
897
898      if (allResultsInitialRender && listeo_core.maps_autofit === 'on' &&
899          Array.isArray(overviewManifest.bounds) && overviewManifest.bounds.length === 4) {
900        var b = overviewManifest.bounds;
901        var overviewBounds = L.latLngBounds([b[0], b[1]], [b[2], b[3]]);
902        if (overviewBounds.isValid()) {
903          allResultsInitialRender = false;
904          // moveend -> scheduleAllResultsManifestRender re-renders at the fitted zoom.
905          map.fitBounds(overviewBounds, { padding: [20, 20] });
906        }
907      }
908
909      var zoom = map.getZoom();
910      var maxZoom = parseInt(overviewManifest.max_zoom, 10);
911      if (!isFinite(maxZoom)) maxZoom = 10;
912      var rows = overviewManifest.zooms[String(Math.min(zoom, maxZoom))];
913      if (zoom > maxZoom || !Array.isArray(rows)) {
914        return false;
915      }
916
917      var viewBounds = map.getBounds().pad(0.25);
918      if (markers && map.hasLayer(markers)) {
919        map.removeLayer(markers);
920      }
921      markers = L.layerGroup();
922      markerArray = [];
923
924      rows.forEach(function(row) {
925        var lat = parseFloat(row[0]);
926        var lng = parseFloat(row[1]);
927        if (!isFinite(lat) || !isFinite(lng) || !viewBounds.contains([lat, lng])) return;
928        var group = {
929          manifestRole: '',
930          count: parseInt(row[2], 10) || 1,
931          minLat: parseFloat(row[3]),
932          minLng: parseFloat(row[4]),
933          maxLat: parseFloat(row[5]),
934          maxLng: parseFloat(row[6])
935        };
936        var layer;
937        if (group.count > 1) {
938          layer = L.marker([lat, lng], { icon: createManifestClusterIcon(group) });
939          layer.on('mouseover', function() {
940            clearTimeout(clusterPrefetchTimer);
941            var clusterCacheToken = getClusterCacheToken(map, group);
942            clusterPrefetchTimer = setTimeout(function() {
943              prefetchClusterListings(map, group, clusterCacheToken);
944            }, 180);
945          });
946          layer.on('mouseout', function() {
947            clearTimeout(clusterPrefetchTimer);
948          });
949          layer.on('click', function() {
950            scheduleFullManifestLoad(map);
951            var clusterCacheToken = getClusterCacheToken(map, group);
952            var navigation = getClusterNavigation(map, group);
953            var navigationFinished = false;
954            var finishNavigation = function() {
955              if (navigationFinished) return;
956              navigationFinished = true;
957              userMovedMap = false;
958              scheduleAllResultsManifestRender(map);
959              updateListingsForMapBounds(map, true, clusterCacheToken);
960            };
961            map.once('zoomend moveend', finishNavigation);
962            if (navigation.useSetView) {
963              map.setView(navigation.center, navigation.zoom);
964            } else {
965              map.fitBounds(navigation.bounds, { padding: [35, 35], maxZoom: navigation.zoom });
966            }
967            setTimeout(finishNavigation, 500);
968          });
969        } else {
970          // Single listing: popup content lives in the full manifest, which replaces
971          // this layer as soon as it arrives.
972          layer = L.marker([lat, lng], {
973            icon: createManifestPointIcon(group, getManifestIconHtml(overviewManifest, row[8]))
974          });
975        }
976        markers.addLayer(layer);
977        markerArray.push(L.marker([lat, lng]));
978      });
979
980      map.addLayer(markers);
981      return true;
982    }
983
984    function ensureAllResultsManifest(map) {
985      if (!usesAllResultsManifest() || allResultsManifest || allResultsManifestLoading) {
986        return;
987      }
988
989      watchAllResultsManifestInitialRender(map);
990      allResultsManifestLoading = true;
991      $.ajax({
992        url: listeo_core.map_all_results_manifest_url,
993        dataType: 'json',
994        cache: true
995      }).done(function(data) {
996        if (!data || !Array.isArray(data.points)) {
997          allResultsManifestFailed = true;
998          stopAllResultsManifestInitialWatchers();
999          return;
1000        }
1001        allResultsManifest = data;
1002        allResultsManifestLoading = false;
1003        manifestInitialRenderAttempts = 0;
1004        var manifestMap = window.map || map;
1005        renderLoadedAllResultsManifest(manifestMap);
1006
1007        // The cold split-map can have a usable Leaflet viewport before its
1008        // internal `_loaded` flag/whenReady callback settles. A real moveend
1009        // already renders correctly in that state, so run that exact render
1010        // path as soon as the manifest arrives instead of waiting for input.
1011        scheduleAllResultsManifestRender(manifestMap);
1012      }).fail(function() {
1013        allResultsManifestFailed = true;
1014        stopAllResultsManifestInitialWatchers();
1015      }).always(function() {
1016        allResultsManifestLoading = false;
1017      });
1018    }
1019
1020    function renderLoadedAllResultsManifest(map) {
1021      if (map) {
1022        manifestInitialRenderMap = map;
1023      }
1024      map = manifestInitialRenderMap || window.map;
1025      if (!map || !allResultsManifest || allResultsManifestFailed || allResultsManifestRendered) {
1026        return;
1027      }
1028
1029      var retry = function() {
1030        if (allResultsManifestRendered || ++manifestInitialRenderAttempts >= 100) return;
1031        clearTimeout(manifestInitialRenderTimer);
1032        manifestInitialRenderTimer = setTimeout(function() {
1033          renderLoadedAllResultsManifest(map);
1034        }, 100);
1035      };
1036
1037      var container = typeof map.getContainer === 'function' ? map.getContainer() : null;
1038      var size = typeof map.getSize === 'function' ? map.getSize() : null;
1039      if (!map._loaded || !container || !size || size.x < 1 || size.y < 1 ||
1040          container.offsetWidth < 1 || container.offsetHeight < 1) {
1041        if (!map._loaded && typeof map.whenReady === 'function') {
1042          map.whenReady(function() {
1043            renderLoadedAllResultsManifest(map);
1044          });
1045        }
1046        retry();
1047        return;
1048      }
1049
1050      if (typeof map.invalidateSize === 'function') {
1051        map.invalidateSize({ animate: false, pan: false });
1052      }
1053
1054      var render = function() {
1055        if (!allResultsManifest || allResultsManifestFailed || allResultsManifestRendered) return;
1056        try {
1057          var points = getManifestRenderPoints(map);
1058          if (points !== null) {
1059            renderAllResultsManifestMap(map, points);
1060          }
1061          if (!allResultsManifestRendered) retry();
1062        } catch (error) {
1063          allResultsManifestFailed = true;
1064          allResultsManifest = null;
1065          if (!markers || !map.hasLayer(markers)) {
1066            markerArray = [];
1067            markers = false;
1068            listingsMap(map);
1069          }
1070        }
1071      };
1072
1073      L.Util.requestAnimFrame(function() {
1074        L.Util.requestAnimFrame(function() {
1075          render();
1076        });
1077      });
1078    }
1079
1080    function splitManifestTokens(value) {
1081      return String(value || '').split('|').filter(function(token) { return token !== ''; });
1082    }
1083
1084    function manifestHasToken(value, wanted) {
1085      wanted = String(wanted || '').toLowerCase();
1086      return !wanted || splitManifestTokens(value).some(function(token) {
1087        return token.toLowerCase() === wanted;
1088      });
1089    }
1090
1091    function escapeManifestHtml(value) {
1092      return $('<div>').text(String(value || '')).html();
1093    }
1094
1095    function getManifestPopupHtml(point) {
1096      var url = escapeManifestHtml(point[11] || '#');
1097      var image = escapeManifestHtml(point[12] || '');
1098      var title = escapeManifestHtml(point[10] || 'Listing');
1099      var address = escapeManifestHtml(point[9] || '');
1100      var rating = parseFloat(point[5] || 0);
1101      var reviews = parseInt(point[13] || 0, 10);
1102
1103      if (image) {
1104        return locationData(url, image, title, '', '', address, rating, reviews, '');
1105      }
1106
1107      return '<div class="leaflet-listing-content"><div class="listing-title">' +
1108        '<h4><a href="' + url + '">' + title + '</a></h4>' +
1109        '<span>' + address + '</span>' +
1110        '</div></div>';
1111    }
1112
1113    function getAllResultsFilterSignature() {
1114      var container = $('#listeo-listings-container');
1115      var fields = $('#listeo_core-search-form').serializeArray().filter(function(field) {
1116        return field.name !== 'search_by_map_move' && field.name.indexOf('map_bounds[') !== 0;
1117      }).map(function(field) {
1118        return field.name + '=' + field.value;
1119      }).sort();
1120      return fields.join('&') +
1121        '|category=' + String(container.data('category') || '') +
1122        '|region=' + String(container.data('region') || '') +
1123        '|feature=' + String(container.data('feature') || '');
1124    }
1125
1126    function getAllResultsFilterState(map) {
1127      var form = $('#listeo_core-search-form');
1128      var serialized = form.serializeArray();
1129      var radiusValue = '';
1130      var state = {
1131        categories: [],
1132        regions: [],
1133        types: [],
1134        features: [],
1135        rating: 0,
1136        price: null,
1137        keyword: '',
1138        location: '',
1139        bounds: allResultsInitialRender ? null : map.getBounds()
1140      };
1141      var supported = true;
1142
1143      serialized.forEach(function(field) {
1144        var name = field.name || '';
1145        var value = String(field.value || '').trim();
1146        // "0" is the placeholder option of taxonomy selects (e.g. tax-region);
1147        // PHP treats it as empty(), so the manifest filter must ignore it too.
1148        if (!value || value === '-1' || value === 'any' || (value === '0' && name.indexOf('tax-') === 0)) {
1149          return;
1150        }
1151
1152        if (name.indexOf('tax-listing_category') === 0) {
1153          state.categories.push(value);
1154        } else if (name.indexOf('tax-region') === 0) {
1155          state.regions.push(value);
1156        } else if (name.indexOf('tax-listing_feature') === 0) {
1157          if (value === 'on') {
1158            var featureMatch = name.match(/\[([^\]]+)\]/);
1159            value = featureMatch ? featureMatch[1] : '';
1160          }
1161          if (value) state.features.push(value);
1162        } else if (name === 'rating-filter') {
1163          state.rating = parseFloat(value) || 0;
1164        } else if (name === '_price_range') {
1165          var range = value.split(',').map(function(part) { return parseFloat(part); });
1166          if (range.length === 2 && !isNaN(range[0]) && !isNaN(range[1])) {
1167            state.price = range;
1168          }
1169        } else if (name === 'keyword_search') {
1170          state.keyword = value.toLowerCase();
1171        } else if (name === 'location_search') {
1172          state.location = value.toLowerCase();
1173        } else if (name === 'listing_type' || name === 'listing_type[]') {
1174          state.types.push(value);
1175        } else if (name === 'search_radius') {
1176          // Only meaningful together with a location - resolved after the loop.
1177          radiusValue = value;
1178        } else if (
1179          name === 'listeo_keyword_search_website' ||
1180          name === '_listeo_keyword_search_interaction' ||
1181          name === 'listing_order' ||
1182          name === 'search_by_map_move' ||
1183          name === 'date_range_mode' ||
1184          name === 'ai_search_hp' ||
1185          name.indexOf('map_bounds[') === 0 ||
1186          name.indexOf('place_viewport[') === 0 ||
1187          name === 'place_type'
1188        ) {
1189          return;
1190        } else {
1191          // Unknown field with an active value (date range, AI search, custom
1192          // form fields...) - the manifest cannot reproduce that filter
1193          // client-side, so the current-results markers stay in charge.
1194          supported = false;
1195        }
1196      });
1197
1198      // An active radius search is geographic; the manifest's plain-text
1199      // location match cannot reproduce it.
1200      if (radiusValue && parseFloat(radiusValue) > 0 &&
1201          (state.location || $('#listeo_core-search-form input[name="_geolocation_lat"]').val())) {
1202        supported = false;
1203      }
1204
1205      if (!state.categories.length) {
1206        var fixedCategory = $('#listeo-listings-container').data('category');
1207        if (fixedCategory) state.categories.push(String(fixedCategory));
1208      }
1209      if (!state.regions.length) {
1210        var fixedRegion = $('#listeo-listings-container').data('region');
1211        if (fixedRegion) state.regions.push(String(fixedRegion));
1212      }
1213      if (!state.features.length) {
1214        var fixedFeature = $('#listeo-listings-container').data('feature');
1215        if (fixedFeature) state.features.push(String(fixedFeature));
1216      }
1217
1218      return supported ? state : null;
1219    }
1220
1221    function getFilteredManifestPoints(map, sourcePoints, ignoreBounds) {
1222      var state = getAllResultsFilterState(map);
1223      if (!state) {
1224        return null;
1225      }
1226
1227      if (ignoreBounds) {
1228        state.bounds = null;
1229      }
1230
1231      var points = Array.isArray(sourcePoints) ? sourcePoints : allResultsManifest.points;
1232      return points.filter(function(point) {
1233        var lat = parseFloat(point[1]);
1234        var lng = parseFloat(point[2]);
1235        if (!isFinite(lat) || !isFinite(lng)) return false;
1236        if (state.bounds && !state.bounds.contains([lat, lng])) return false;
1237        if (state.categories.length && !state.categories.some(function(value) { return manifestHasToken(point[3], value); })) return false;
1238        if (state.regions.length && !state.regions.some(function(value) { return manifestHasToken(point[4], value); })) return false;
1239        if (state.types.length && !state.types.some(function(value) { return manifestHasToken(point[14], value); })) return false;
1240        if (state.rating && parseFloat(point[5] || 0) < state.rating) return false;
1241        if (state.price) {
1242          var priceMin = parseFloat(point[6] || 0);
1243          var priceMax = parseFloat(point[7] || point[6] || 0);
1244          if (priceMax < state.price[0] || priceMin > state.price[1]) return false;
1245        }
1246        if (state.features.length && !state.features.every(function(value) { return manifestHasToken(point[8], value); })) return false;
1247        if (state.location && String(point[9] || '').toLowerCase().indexOf(state.location) === -1) return false;
1248        if (state.keyword && String(point[10] || '').toLowerCase().indexOf(state.keyword) === -1) return false;
1249        return true;
1250      });
1251    }
1252
1253    function getManifestRenderPoints(map) {
1254      var filterSignature = getAllResultsFilterSignature();
1255      if (activeManifestSubset && activeManifestFilterSignature !== filterSignature) {
1256        activeManifestSubset = null;
1257        activeManifestFilterSignature = '';
1258      }
1259
1260      return getFilteredManifestPoints(
1261        map,
1262        activeManifestSubset || allResultsManifest.points,
1263        !!activeManifestSubset
1264      );
1265    }
1266
1267    function clusterManifestPoints(points, zoom, manifestRole) {
1268      var gridSize = parseInt(listeo_core.map_cluster_grid_size, 10) || 80;
1269      var worldSize = 256 * Math.pow(2, zoom);
1270      var groups = {};
1271
1272      points.forEach(function(point) {
1273        var lat = parseFloat(point[1]);
1274        var lng = parseFloat(point[2]);
1275        var mercatorLat = Math.max(-85.05112878, Math.min(85.05112878, lat));
1276        var sinLat = Math.sin(mercatorLat * Math.PI / 180);
1277        var pixelX = ((lng + 180) / 360) * worldSize;
1278        var pixelY = (0.5 - Math.log((1 + sinLat) / (1 - sinLat)) / (4 * Math.PI)) * worldSize;
1279        var key = Math.floor(pixelX / gridSize) + ':' + Math.floor(pixelY / gridSize);
1280
1281        if (!groups[key]) {
1282          groups[key] = {
1283            manifestRole: manifestRole || '',
1284            count: 0,
1285            latSum: 0,
1286            lngSum: 0,
1287            minLat: lat,
1288            maxLat: lat,
1289            minLng: lng,
1290            maxLng: lng,
1291            point: point,
1292            points: []
1293          };
1294        }
1295        var group = groups[key];
1296        group.points.push(point);
1297        group.count++;
1298        group.latSum += lat;
1299        group.lngSum += lng;
1300        group.minLat = Math.min(group.minLat, lat);
1301        group.maxLat = Math.max(group.maxLat, lat);
1302        group.minLng = Math.min(group.minLng, lng);
1303        group.maxLng = Math.max(group.maxLng, lng);
1304      });
1305
1306      return Object.keys(groups).map(function(key) {
1307        return groups[key];
1308      });
1309    }
1310
1311    function getManifestRenderGroups(map, focusPoints) {
1312      var focusRole = activeManifestSubset ? 'focus' : '';
1313      var focusGroups = clusterManifestPoints(focusPoints, map.getZoom(), focusRole);
1314      if (!activeManifestSubset) {
1315        return focusGroups;
1316      }
1317
1318      var focusIds = {};
1319      activeManifestSubset.forEach(function(point) {
1320        focusIds[String(point[0])] = true;
1321      });
1322      var backgroundPoints = getFilteredManifestPoints(map, allResultsManifest.points, false);
1323      if (backgroundPoints === null) {
1324        return focusGroups;
1325      }
1326      backgroundPoints = backgroundPoints.filter(function(point) {
1327        return !focusIds[String(point[0])];
1328      });
1329
1330      return clusterManifestPoints(backgroundPoints, map.getZoom(), 'background').concat(focusGroups);
1331    }
1332
1333    function clearManifestDrilldown() {
1334      activeManifestSubset = null;
1335      activeManifestFilterSignature = '';
1336    }
1337
1338    function scheduleAllResultsManifestRender(map) {
1339      if (!usesAllResultsManifest()) {
1340        return;
1341      }
1342      if (!allResultsManifest) {
1343        if (overviewManifest && !allResultsManifestRendered) {
1344          clearTimeout(manifestMapRenderTimer);
1345          manifestMapRenderTimer = setTimeout(function() {
1346            renderOverviewManifestMap(map);
1347          }, 35);
1348        }
1349        return;
1350      }
1351
1352      clearTimeout(manifestMapRenderTimer);
1353      manifestMapRenderTimer = setTimeout(function() {
1354        var points = getManifestRenderPoints(map);
1355        if (points !== null) {
1356          renderAllResultsManifestMap(map, points);
1357        }
1358      }, 35);
1359    }
1360
1361    function bindAllResultsManifestMapEvents(map) {
1362      if (manifestMapEventsBound) {
1363        return;
1364      }
1365      manifestMapEventsBound = true;
1366
1367      map.on('dragstart', function() {
1368        clearManifestDrilldown();
1369      });
1370      map.on('zoomstart', function() {
1371        if (!clusterNavigationMove) {
1372          clearManifestDrilldown();
1373        }
1374      });
1375      map.on('zoomend moveend', function() {
1376        scheduleAllResultsManifestRender(map);
1377      });
1378    }
1379
1380    function renderAllResultsManifestMap(map, points) {
1381      if (markers && map.hasLayer(markers)) {
1382        map.removeLayer(markers);
1383      }
1384      markers = L.layerGroup();
1385      markerArray = [];
1386      var groups = getManifestRenderGroups(map, points);
1387
1388      groups.forEach(function(group) {
1389        var center = [group.latSum / group.count, group.lngSum / group.count];
1390        var layer;
1391
1392        if (group.count > 1) {
1393          layer = L.marker(center, {
1394            icon: createManifestClusterIcon(group),
1395            zIndexOffset: group.manifestRole === 'focus' ? 1000 : 0
1396          });
1397          layer.on('mouseover', function() {
1398            clearTimeout(clusterPrefetchTimer);
1399            var clusterCacheToken = getClusterCacheToken(map, group);
1400            clusterPrefetchTimer = setTimeout(function() {
1401              prefetchClusterListings(map, group, clusterCacheToken);
1402            }, 180);
1403          });
1404          layer.on('mouseout', function() {
1405            clearTimeout(clusterPrefetchTimer);
1406          });
1407          layer.on('click', function() {
1408            activeManifestSubset = group.points;
1409            activeManifestFilterSignature = getAllResultsFilterSignature();
1410            clusterNavigationMove = true;
1411            var clusterCacheToken = getClusterCacheToken(map, group);
1412            var navigation = getClusterNavigation(map, group);
1413            var navigationFinished = false;
1414            var finishNavigation = function() {
1415              if (navigationFinished) return;
1416              navigationFinished = true;
1417              clusterNavigationMove = false;
1418              userMovedMap = false;
1419              clearTimeout(manifestMapRenderTimer);
1420              L.Util.requestAnimFrame(function() {
1421                var points = getManifestRenderPoints(map);
1422                if (points !== null) {
1423                  renderAllResultsManifestMap(map, points);
1424                }
1425                updateListingsForMapBounds(map, true, clusterCacheToken);
1426              });
1427            };
1428            map.once('zoomend moveend', finishNavigation);
1429            if (navigation.useSetView) {
1430              map.setView(navigation.center, navigation.zoom);
1431            } else {
1432              map.fitBounds(navigation.bounds, { padding: [35, 35], maxZoom: navigation.zoom });
1433            }
1434            setTimeout(finishNavigation, 500);
1435          });
1436        } else {
1437          layer = L.marker(center, {
1438            icon: createManifestPointIcon(group, getManifestIconHtml(allResultsManifest, group.point && group.point[15])),
1439            zIndexOffset: group.manifestRole === 'focus' ? 1000 : 0
1440          }).bindPopup(
1441            getManifestPopupHtml(group.point),
1442            { maxWidth: 270, className: 'leaflet-infoBox' }
1443          );
1444        }
1445
1446        markers.addLayer(layer);
1447        markerArray.push(L.marker(center));
1448      });
1449
1450      map.addLayer(markers);
1451      allResultsManifestRendered = true;
1452      stopAllResultsManifestInitialWatchers();
1453      if (allResultsInitialRender && points.length && listeo_core.maps_autofit === 'on') {
1454        var allBounds = L.latLngBounds(points.map(function(point) {
1455          return [parseFloat(point[1]), parseFloat(point[2])];
1456        }));
1457        if (allBounds.isValid()) {
1458          map.fitBounds(allBounds, { padding: [20, 20] });
1459        }
1460      }
1461      allResultsInitialRender = false;
1462    }
1463
1464    function listingsMap(map){
1465        if (usesAllResultsManifest()) {
1466          bindAllResultsManifestMapEvents(map);
1467          ensureOverviewManifest(map);
1468          if (allResultsManifest) {
1469            if (!allResultsManifestRendered) {
1470              renderLoadedAllResultsManifest(map);
1471            }
1472            var manifestPoints = getManifestRenderPoints(map);
1473            if (allResultsManifestRendered && manifestPoints !== null) {
1474              renderAllResultsManifestMap(map, manifestPoints);
1475              return;
1476            }
1477          } else if (overviewManifest && renderOverviewManifestMap(map)) {
1478            return;
1479          }
1480        }
1481
1482        markers = L.markerClusterGroup({
1483            spiderfyOnMaxZoom: true,
1484            showCoverageOnHover: false,
1485
1486          });
1487        locations = getMarkers();
1488
1489        for (var i = 0; i < locations.length; i++) {
1490
1491          // Validate and normalize coordinates before creating marker
1492          var lat = locations[i][2];
1493          var lng = locations[i][1];
1494
1495          var validCoords = isValidCoordinate(lat, lng);
1496          if (!validCoords) {
1497            console.warn('Listeo Map: Skipping marker for listing at index ' + i + ' due to invalid coordinates');
1498            continue; // Skip this marker and continue with the next one
1499          }
1500
1501          try {
1502            var listeoIcon = L.divIcon({
1503                iconAnchor: [20, 51], // point of the icon which will correspond to marker's location
1504                popupAnchor: [0, -51],
1505                className: 'listeo-marker-icon',
1506                html:  '<div class="marker-container">'+
1507                                 '<div class="marker-card">'+
1508                                    '<div class="front face">' + locations[i][4] + '</div>'+
1509                                    '<div class="back face">' + locations[i][4] + '</div>'+
1510                                    '<div class="marker-arrow"></div>'+
1511                                 '</div>'+
1512                               '</div>'
1513
1514              }
1515            );
1516              var popupOptions =
1517                {
1518                'maxWidth': '270',
1519                'className' : 'leaflet-infoBox'
1520                }
1521
1522              marker = new L.marker([validCoords.lat, validCoords.lng], {
1523                  icon: listeoIcon,
1524
1525                })
1526                .bindPopup(locations[i][0],popupOptions );
1527                //.addTo(map);
1528                marker.on('click', function(e){
1529                  //alert('test');
1530                 // L.DomUtil.addClass(marker._icon, 'clicked');
1531                });
1532                // map.on('popupopen', function (e) {
1533                //   L.DomUtil.addClass(e.popup._source._icon, 'clicked');
1534
1535
1536                // }).on('popupclose', function (e) {
1537                //   if(e.popup){
1538                //     L.DomUtil.removeClass(e.popup._source._icon, 'clicked');
1539                //   }
1540
1541                // });
1542                markers.addLayer(marker);
1543
1544                markerArray.push(L.marker([validCoords.lat, validCoords.lng]));
1545          } catch (error) {
1546            // Continue with next marker instead of breaking the entire map
1547            continue;
1548          }
1549        }
1550        map.addLayer(markers);
1551
1552    
1553        if(markerArray.length > 0 ){
1554          group = L.featureGroup(markerArray);
1555           if (
1556             listeo_core.maps_autofit == "on" &&
1557             !$('#listeo_core-search-form input[name="search_by_map_move"]')
1558               .length
1559           ) {
1560             map.fitBounds(group.getBounds());
1561           }
1562         
1563        }
1564    }
1565
1566    $( '#listeo-listings-container' ).on( 'update_results_success', function ( event, data ) {
1567        if (window.map && usesAllResultsManifest() && !allResultsManifest && !isAllResultsFilterEmpty(window.map)) {
1568          scheduleFullManifestLoad(window.map);
1569        }
1570        userMovedMap = false;
1571        ignoreMapEvents = true;
1572        setTimeout(function(){ ignoreMapEvents = false; }, 1500);
1573        if(window.map){
1574            var preserveManifestDrilldown = usesAllResultsManifest() &&
1575              allResultsManifest &&
1576              activeManifestSubset &&
1577              activeManifestFilterSignature === getAllResultsFilterSignature();
1578            if (!preserveManifestDrilldown) {
1579              map.closePopup();
1580              if (markers && map.hasLayer(markers)) {
1581                map.removeLayer(markers);
1582              }
1583              markerArray = [];
1584              markers = false;
1585              listingsMap(map);
1586
1587              // With zero result markers there is nothing for maps_autofit to
1588              // fit. Keep the map on the searched location instead of leaving
1589              // it wherever it was (or at the default position).
1590              if (
1591                markerArray.length === 0 &&
1592                data && data.map_center &&
1593                isFinite(parseFloat(data.map_center.lat)) &&
1594                isFinite(parseFloat(data.map_center.lng)) &&
1595                !$('#listeo_core-search-form input[name="search_by_map_move"]').length
1596              ) {
1597                map.flyTo(
1598                  [parseFloat(data.map_center.lat), parseFloat(data.map_center.lng)],
1599                  Math.max(map.getZoom(), 10)
1600                );
1601              }
1602            }
1603            
1604            // Update user location marker if search data includes location and radius
1605            if (data && data.user_location && data.user_location.lat && data.user_location.lng && data.user_location.radius) {
1606              // Check if this is a new location (different from current)
1607              var isNewLocation = !userLocationMarker || 
1608                Math.abs(userLocationMarker.getLatLng().lat - data.user_location.lat) > 0.0001 ||
1609                Math.abs(userLocationMarker.getLatLng().lng - data.user_location.lng) > 0.0001;
1610              
1611              // Only reset firstShow flag if it's a genuinely new location
1612              if (isNewLocation) {
1613                userLocationFirstShow = true;
1614              }
1615              
1616              updateUserLocationOnMap(
1617                data.user_location.lat,
1618                data.user_location.lng,
1619                data.user_location.radius,
1620                data.user_location.address
1621              );
1622            } else {
1623              // Check form data for location search
1624              var locationSearch = $('#location_search').val();
1625              var radiusValue = $('.distance-radius').val() || $('#search_radius').val();
1626              
1627              if (locationSearch && radiusValue && (listeo_core.maps_api_server || listeo_core.geoapify_maps_api_server)) {
1628                // Clear user location for now - will be handled by the enhanced AJAX response
1629                // This ensures we don't show stale data while new geocoding happens
1630              } else {
1631                // Clear user location if no search location or radius
1632                userLocationLayer.clearLayers();
1633                userLocationFirstShow = true; // Reset for next location search
1634              }
1635            }
1636        }
1637      
1638    });
1639
1640    
1641    var map_id =  document.querySelectorAll('div#map');
1642
1643    if (map_id.length > 0) {
1644      listingsMap(map);
1645    }
1646
1647    if(listeo_core.map_provider != 'google') {
1648      $('.show-map-button').on('click', function(event) {
1649       event.preventDefault(); 
1650       $(".hide-map-on-mobile").toggleClass("map-active"); 
1651       var text_enabled = $(this).data('enabled');
1652       var text_disabled = $(this).data('disabled');
1653       if( $(".hide-map-on-mobile").hasClass('map-active')){
1654        $(this).text(text_disabled);
1655          
1656          // map.removeLayer(markers);
1657          // markerArray = [];
1658          // markers = false;
1659          // map.closePopup();
1660          // listingsMap(map);
1661       } else {
1662        $(this).text(text_enabled);
1663       }
1664      });
1665    }
1666
1667   
1668
1669
1670    function submitPropertyMap(){
1671
1672        window.L_DISABLE_3D = true
1673
1674        var curLocation = [-33.92, 151.25]; // Default location (Sydney, Australia)
1675
1676        if(listeo_core.submitCenterPoint) {
1677          var latlngStr = listeo_core.submitCenterPoint.split(",",2);
1678          var lat = parseFloat(latlngStr[0]);
1679          var lng = parseFloat(latlngStr[1]);
1680
1681          var validCoords = isValidCoordinate(lat, lng);
1682          if (validCoords) {
1683            curLocation = [validCoords.lat, validCoords.lng];
1684          }
1685        }
1686
1687        if($('#_geolocation_long').val() && $('#_geolocation_lat').val()) {
1688          var userLat = parseFloat($('#_geolocation_lat').val());
1689          var userLng = parseFloat($('#_geolocation_long').val());
1690
1691          var validCoords = isValidCoordinate(userLat, userLng);
1692          if (validCoords) {
1693            curLocation = [validCoords.lat, validCoords.lng];
1694          }
1695        }
1696        var listeoIcon = L.divIcon({
1697            iconAnchor: [20, 51], // point of the icon which will correspond to marker's location
1698            popupAnchor: [0, -51],
1699            className: 'listeo-marker-icon',
1700            html:  '<div class="marker-container no-marker-icon ">'+
1701                             '<div class="marker-card">'+
1702                                '<div class="front face"><i class="fa fa-map-pin"></i></div>'+
1703                                '<div class="back face"><i class="fa fa-map-pin"></i></div>'+
1704                                '<div class="marker-arrow"></div>'+
1705                             '</div>'+
1706                           '</div>'
1707            
1708          }
1709        );
1710        var mapOptions = {
1711            center: curLocation,
1712            zoom: 8,
1713            zoomControl: false,
1714            gestureHandling: false
1715         }
1716        window.submit_map = L.map('submit_map',mapOptions);
1717
1718
1719        var zoomOptions = {
1720           zoomInText: '<i class="fa fa-plus" aria-hidden="true"></i>',
1721           zoomOutText: '<i class="fa fa-minus" aria-hidden="true"></i>',
1722        };
1723        // Creating zoom control
1724        var zoom = L.control.zoom(zoomOptions);
1725        zoom.addTo(submit_map);
1726        
1727        
1728        switch(listeo_core.map_provider) {
1729          case 'osm':
1730              L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
1731                  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
1732              }).addTo(submit_map);
1733          break;
1734          
1735          case 'google':
1736
1737              var googleMutantOptions = {
1738                type: 'roadmap', // valid values are 'roadmap', 'satellite', 'terrain' and 'hybrid'
1739                maxZoom: 18
1740              };
1741
1742              // Add Map ID if configured (for Cloud Styled Maps)
1743              if (listeo_core.google_maps_id && listeo_core.google_maps_id !== '') {
1744                googleMutantOptions.mapId = listeo_core.google_maps_id;
1745              }
1746
1747              whenGoogleMapsReady(function() {
1748                L.gridLayer.googleMutant(googleMutantOptions).addTo(submit_map);
1749              });
1750
1751          break;
1752          
1753          case 'mapbox':
1754      
1755              var accessToken = listeo_core.mapbox_access_token;
1756              var mapbox_style_url = listeo_core.mapbox_style_url;
1757              
1758              if(listeo_core.mapbox_retina){
1759                L.tileLayer(mapbox_style_url + accessToken, {
1760                    attribution: " &copy;  <a href='https://www.mapbox.com/about/maps/'>Mapbox</a> &copy;  <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> <strong><a href='https://www.mapbox.com/map-feedback/' target='_blank'>Improve this map</a></strong>",
1761                    maxZoom: 18,
1762                    zoomOffset: -1,
1763                    //detectRetina: true,
1764                    tileSize: 512,
1765                    
1766                }).addTo(submit_map);
1767              } else {
1768
1769                L.tileLayer(mapbox_style_url + accessToken, {
1770                    attribution: " &copy;  <a href='https://www.mapbox.com/about/maps/'>Mapbox</a> &copy;  <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> <strong><a href='https://www.mapbox.com/map-feedback/' target='_blank'>Improve this map</a></strong>",
1771                    maxZoom: 18,
1772                    //detectRetina: true,
1773                    id: 'mapbox.streets',
1774                }).addTo(submit_map);
1775              }
1776
1777             
1778          break;
1779
1780          case 'bing':
1781              L.tileLayer.bing(listeo_core.bing_maps_key).addTo(submit_map)
1782          break;
1783
1784          case 'thunderforest':
1785              var tileUrl = 'https://tile.thunderforest.com/cycle/{z}/{x}/{y}{r}.png?apikey='+listeo_core.thunderforest_api_key,
1786              layer = new L.TileLayer(tileUrl, {maxZoom: 18});
1787              submit_map.addLayer(layer);
1788          break;
1789
1790          case 'here':
1791              L.tileLayer.here({appId: listeo_core.here_app_id, appCode: listeo_core.here_app_code}).addTo(submit_map);
1792          break;
1793        }
1794
1795   
1796      
1797
1798        submit_map.scrollWheelZoom.disable();
1799
1800     
1801        
1802        window.submit_marker = new L.marker(curLocation, {
1803            draggable: 'true',
1804            icon: listeoIcon,
1805        });
1806
1807        if(listeo_core.address_provider == 'osm'){
1808          if(listeo_core.country){
1809            var geocoder = new L.Control.Geocoder.Nominatim( {
1810                geocodingQueryParams: {countrycodes: listeo_core.country} //accept-language: 'en'
1811            });
1812          } else {
1813            var geocoder = new L.Control.Geocoder.Nominatim();  
1814          } 
1815          
1816          var output = [];
1817          $("#_address").attr('autocomplete', 'off').after('<div id="leaflet-geocode
1817-cont"><ul></ul></div>');
1818          // $("#_address").on("focusout",function(e) {
1819          //     $("#leaflet-geocode-cont").removeClass('active');
1820          // });
1821          $("#_address").on("focusin",function(e) {
1822              $('.type-and-hit-enter').addClass('tip-visible');
1823
1824              setTimeout(function(){
1825                     $('.type-and-hit-enter').removeClass('tip-visible');
1826                    //....and whatever else you need to do
1827               }, 3000);
1828          });
1829          $("#_address").on("keydown",function search(e) {
1830
1831            $("#leaflet-geocode-cont").removeHighlight();
1832              if(e.keyCode == 13) {
1833
1834                var query = $(this).val();
1835                // if query lenght is less than 3 characters, do not send request
1836                if(query.length < 3){ return false; }
1837                if(query){
1838
1839
1840                  geocoder.geocode(query, function(results) { 
1841                    
1842                    for (var i = 0; i < results.length; i++) {
1843                      
1844                      output.push('<li data-latitude="'+results[i].center.lat+'" data-longitude="'+results[i].center.lng+'" >'+results[i].name+'</li>');
1845                    }
1846                    output.push('<li class="powered-by-osm">Powered by <strong>OpenStreetMap</strong></li>');
1847                    $("#leaflet-geocode-cont").addClass('active');
1848                    $('#autocomplete-container').addClass("osm-dropdown-active");
1849                    $('#leaflet-geocode-cont ul').html(output);
1850                    var txt_to_hl = query.split(' ');
1851                    txt_to_hl.forEach(function (item) {
1852                      $('#leaflet-geocode-cont ul').highlight(item);
1853                    });
1854                    output = [];
1855                  });
1856                }
1857                if ($('#_address:focus').length){ return false; }
1858              }
1859          });
1860
1861          $(".form-field-_address-container").on( "click", "#leaflet-geocode-cont ul li", function(e) {
1862              
1863              var newLatLng = new L.LatLng($(this).data('latitude'), $(this).data('longitude'));
1864              $("#_address").val($(this).text());
1865              submit_marker.setLatLng(newLatLng).update(); 
1866              submit_map.panTo(newLatLng);
1867              $("#_geolocation_lat").val($(this).data('latitude'));
1868              $("#_geolocation_long").val($(this).data('longitude'));
1869              $("#leaflet-geocode-cont").removeClass('active');
1870              $('#autocomplete-container').removeClass("osm-dropdown-active");
1871          });
1872        
1873          submit_marker.on('dragend', function(event) {
1874              var position = submit_marker.getLatLng();
1875              submit_marker.setLatLng(position, {
1876                draggable: 'true'
1877              }).bindPopup(position).update();
1878
1879              geocoder.reverse(position, submit_map.options.crs.scale(submit_map.getZoom()), function(results) {
1880                if (results && results.length && results[0] && results[0].name) {
1881                  $("#_address").val(results[0].name);
1882                }
1883              });
1884              $("#_geolocation_lat").val(position.lat);
1885              $("#_geolocation_long").val(position.lng).keyup();
1886          });
1887    }
1888    
1889
1890
1891        $("#_geolocation_lat").change(function() {
1892            if( $("#_geolocation_long").val() ) {
1893              var lat = parseFloat($("#_geolocation_lat").val());
1894              var lng = parseFloat($("#_geolocation_long").val());
1895
1896              var validCoords = isValidCoordinate(lat, lng);
1897              if (validCoords) {
1898                var position = [validCoords.lat, validCoords.lng];
1899                submit_marker.setLatLng(position, {
1900                  draggable: 'true'
1901                }).bindPopup(position).update();
1902                submit_map.panTo(position);
1903              }
1904            }
1905
1906        });
1907        $("#_geolocation_long").change(function() {
1908            if( $("#_geolocation_lat").val() ) {
1909              var lat = parseFloat($("#_geolocation_lat").val());
1910              var lng = parseFloat($("#_geolocation_long").val());
1911
1912              var validCoords = isValidCoordinate(lat, lng);
1913              if (validCoords) {
1914                var position = [validCoords.lat, validCoords.lng];
1915                submit_marker.setLatLng(position, {
1916                  draggable: 'true'
1917                }).bindPopup(position).update();
1918                submit_map.panTo(position);
1919              }
1920            }
1921
1922        });
1923
1924        submit_map.addLayer(submit_marker);
1925
1926        
1927    };
1928
1929    var submit_map_cont =  document.getElementById('submit_map');
1930    if (typeof(submit_map_cont) != 'undefined' && submit_map_cont != null) {
1931        submitPropertyMap();
1932    }
1933      
1934
1935
1936    function singleListingMap() {
1937
1938        var lng = parseFloat($( '#singleListingMap' ).data('longitude'));
1939        var lat =  parseFloat($( '#singleListingMap' ).data('latitude'));
1940
1941        // Validate and normalize coordinates before initializing map
1942        var validCoords = isValidCoordinate(lat, lng);
1943        if (!validCoords) {
1944          console.error('Listeo Map: Cannot display single listing map - invalid coordinates', {lat: lat, lng: lng});
1945          return;
1946        }
1947
1948        try {
1949          var singleMapIco =  "<i class='"+$('#singleListingMap').data('map-icon')+"'></i>";
1950          if($('#singleListingMap').data('map-icon-svg')){
1951            var   singleMapIco = $('#singleListingMap').data('map-icon-svg');
1952          }
1953
1954          var map_single;
1955          var listeoIcon = L.divIcon({
1956              iconAnchor: [20, 51], // point of the icon which will correspond to marker's location
1957              popupAnchor: [0, -51],
1958              className: 'listeo-marker-icon',
1959              html:  '<div class="marker-container no-marker-icon ">'+
1960                               '<div class="marker-card">'+
1961                                  '<div class="front face">' + singleMapIco + '</div>'+
1962                                  '<div class="back face">' + singleMapIco + '</div>'+
1963                                  '<div class="marker-arrow"></div>'+
1964                               '</div>'+
1965                             '</div>'
1966
1967            }
1968          );
1969          var mapOptions = {
1970              center: [validCoords.lat, validCoords.lng],
1971              zoom: listeo_core.maps_single_zoom,
1972              zoomControl: false,
1973              gestureHandling: false
1974           }
1975
1976          map_single = L.map('singleListingMap',mapOptions);
1977          var zoomOptions = {
1978             zoomInText: '<i class="fa fa-plus" aria-hidden="true"></i>',
1979             zoomOutText: '<i class="fa fa-minus" aria-hidden="true"></i>',
1980          };
1981          // Creating zoom control
1982          var zoom = L.control.zoom(zoomOptions);
1983          zoom.addTo(map_single);
1984
1985          map_single.scrollWheelZoom.disable();
1986          if($('#singleListingMap-container').hasClass('circle-point')) {
1987             marker = new L.circleMarker([validCoords.lat, validCoords.lng], {
1988                  radius:40
1989            }).addTo(map_single);
1990          } else {
1991            marker = new L.marker([validCoords.lat, validCoords.lng], {
1992                  icon: listeoIcon,
1993            }).addTo(map_single);
1994
1995          }
1996        } catch (error) {
1997          console.error('Listeo Map: Error creating single listing map', error);
1998          return;
1999        }
2000        
2001        switch(listeo_core.map_provider) {
2002          case 'osm':
2003              L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
2004                  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
2005              }).addTo(map_single);
2006            break;
2007         case 'google':
2008
2009              var googleMutantOptions = {
2010                type: 'roadmap', // valid values are 'roadmap', 'satellite', 'terrain' and 'hybrid'
2011                maxZoom: 18
2012              };
2013
2014              // Add Map ID if configured (for Cloud Styled Maps)
2015              if (listeo_core.google_maps_id && listeo_core.google_maps_id !== '') {
2016                googleMutantOptions.mapId = listeo_core.google_maps_id;
2017              }
2018
2019              whenGoogleMapsReady(function() {
2020                L.gridLayer.googleMutant(googleMutantOptions).addTo(map_single);
2021              });
2022
2023            break;
2024
2025       
2026          case 'mapbox':
2027              var accessToken = listeo_core.mapbox_access_token;
2028              var mapbox_style_url = listeo_core.mapbox_style_url;
2029              
2030              if(listeo_core.mapbox_retina){
2031                L.tileLayer(mapbox_style_url + accessToken, {
2032                    attribution: " &copy;  <a href='https://www.mapbox.com/about/maps/'>Mapbox</a> &copy;  <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> <strong><a href='https://www.mapbox.com/map-feedback/' target='_blank'>Improve this map</a></strong>",
2033                    maxZoom: 18,
2034                    zoomOffset: -1,
2035                    //detectRetina: true,
2036                    tileSize: 512,
2037                    
2038                }).addTo(map_single);
2039              } else {
2040
2041                L.tileLayer(mapbox_style_url + accessToken, {
2042                    attribution: " &copy;  <a href='https://www.mapbox.com/about/maps/'>Mapbox</a> &copy;  <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> <strong><a href='https://www.mapbox.com/map-feedback/' target='_blank'>Improve this map</a></strong>",
2043                    maxZoom: 18,
2044                    //detectRetina: true,
2045                    id: 'mapbox.streets',
2046                }).addTo(map_single);
2047              }
2048
2049                
2050            break;
2051
2052          case 'bing':
2053              L.tileLayer.bing(listeo_core.bing_maps_key).addTo(map_single)
2054          break;
2055
2056          case 'thunderforest':
2057              var tileUrl = 'https://tile.thunderforest.com/cycle/{z}/{x}/{y}{r}.png?apikey='+listeo_core.thunderforest_api_key,
2058              layer = new L.TileLayer(tileUrl, {maxZoom: 18});
2059              map_single.addLayer(layer);
2060          break;
2061
2062          case 'here':
2063              L.tileLayer.here({appId: listeo_core.here_app_id, appCode: listeo_core.here_app_code}).addTo(map_single);
2064          break;
2065        }
2066
2067         $(window).on('load resize', function () {
2068              setTimeout(function(){ map_single.invalidateSize()}, 100);
2069            });
2070    }
2071
2072
2073
2074        // Single Listing Map Init
2075        var single_map_cont =  document.getElementById('singleListingMap');
2076
2077        if (typeof(single_map_cont) != 'undefined' && single_map_cont != null) {
2078            
2079            singleListingMap();
2080           
2081     
2082        }
2083        
2084        
2085        
2086if(listeo_core.address_provider == 'osm'){
2087        if(listeo_core.country){
2088          var geocoder = new L.Control.Geocoder.Nominatim( {
2089              geocodingQueryParams: {countrycodes: listeo_core.country}
2090          });
2091        } else {
2092          var geocoder = new L.Control.Geocoder.Nominatim();  
2093        } 
2094        var output = [];
2095        $("#location_search").attr('autocomplete', 'off').after('<div id="leaflet-geocode-cont"><ul></ul></div>')
2096        var liSelected;
2097        var next;
2098        // OSM TIP
2099        $("#location_search")
2100        .on("mouseover",function() {
2101            if ( $(this).val().length < 10 ) {
2102                $('.type-and-hit-enter').addClass('tip-visible');
2103            } 
2104        }).on("mouseout",function(e) {
2105           setTimeout(function(){
2106                 $('.type-and-hit-enter').removeClass('tip-visible');
2107           }, 350);
2108        }).on("keyup",function(e) {
2109
2110            if ( $(this).val().length < 10 ) {
2111                $('.type-and-hit-enter').addClass('tip-visible');
2112            } 
2113            if ( $(this).val().length > 10 ) {
2114                $('.type-and-hit-enter').removeClass('tip-visible tip-visible-focusin');
2115            }
2116            
2117            if(e.which === 40 || e.which === 38 ) {
2118              
2119              
2120            } else {
2121              $('#leaflet-geocode-cont ul li.selected').removeClass('selected');
2122            }
2123            // if(e.which !== 38 ) {
2124            //   console.log(e.which);
2125            //   //$('#leaflet-geocode-cont ul li.selected').removeClass('selected');
2126            // }
2127
2128        }).on("keydown",function(e) {
2129              var li = $('#leaflet-geocode-cont ul li');
2130             if(e.which === 40){
2131              
2132                if(liSelected){
2133                    liSelected.removeClass('selected');
2134                    next = liSelected.next();
2135                    if(next.length > 0){
2136                        liSelected = next.addClass('selected');
2137                    }else{
2138                        liSelected = li.eq(0).addClass('selected');
2139                    }
2140                }else{
2141                    liSelected = li.eq(0).addClass('selected');
2142                }
2143            }else if(e.which === 38){
2144                if(liSelected){
2145                    liSelected.removeClass('selected');
2146                    next = liSelected.prev();
2147                    if(next.length > 0){
2148                        liSelected = next.addClass('selected');
2149                    }else{
2150                        liSelected = li.last().addClass('selected');
2151                    }
2152                }else{
2153                    liSelected = li.last().addClass('selected');
2154                }
2155            }
2156          
2157        });
2158
2159        $("#location_search").on("focusin",function() {
2160            if ( $(this).val().length < 10 ) {
2161                $('.type-and-hit-enter').addClass('tip-visible-focusin');
2162            }
2163            if ( $(this).val().length > 10 ) {
2164                $('.type-and-hit-enter').removeClass('tip-visible-focusin');
2165            }
2166        }).on("focusout",function() {
2167            setTimeout(function(){
2168                $('.type-and-hit-enter').removeClass('tip-visible tip-visible-focusin');
2169            }, 350);
2170            if( $(this).val() == 0 ) {
2171              $('div#listeo-listings-container' ).triggerHandler( 'update_results', [ 1, false ] );
2172            }
2173        });
2174        
2175        $(".location .fa-map-marker").on("mouseover",function() {
2176            $('.type-and-hit-enter').removeClass('tip-visible-focusin tip-visible');
2177        })
2178        
2179        $('.type-and-click-btn').on("click",function search(e) {
2180
2181             var query = $('#_address').val();
2182             //remove any empty space from start and end of the 'query' string
2183
2184            query = query.replace(/^\s+|\s+$/gm,'');
2185             if (query.length < 3) {
2186               return false;
2187             }
2188              if(query){
2189                geocoder.geocode(query, function(results) { 
2190                  
2191                  for (var i = 0; i < results.length; i++) {
2192                    
2193                    output.push('<li data-latitude="'+results[i].center.lat+'" data-longitude="'+results[i].center.lng+'" >'+results[i].name+'</li>');
2194                  }
2195                  output.push('<li class="powered-by-osm">Powered by <strong>OpenStreetMap</strong></li>');
2196                  $("#leaflet-geocode-cont").addClass('active');
2197                  $('#autocomplete-container').addClass("osm-dropdown-active");
2198                  $('#leaflet-geocode-cont ul').html(output);
2199                  var txt_to_hl = query.split(' ');
2200                  txt_to_hl.forEach(function (item) {
2201                    $('#leaflet-geocode-cont ul').highlight(item);
2202                  });
2203                  output = [];
2204                });
2205              }
2206        });
2207        
2208        // Clear viewport fields when location input is cleared/emptied
2209        $("#location_search").on('input change', function() {
2210            var locationValue = $(this).val().trim();
2211            if (locationValue === '') {
2212                var form = $('#listeo_core-search-form');
2213                form.find('input[name^="place_viewport"]').remove();
2214                form.find('input[name="place_type"]').remove();
2215            }
2216        });
2217
2218        $("#location_search").on("keydown",function search(e) {
2219
2220
2221            if(e.keyCode == 13) {
2222                if($('#leaflet-geocode-cont ul li.selected').length>0){
2223                  $('#leaflet-geocode-cont ul li.selected').trigger('click').removeClass('selected');
2224
2225                 return;
2226
2227                }
2228
2229                var query = $(this).val();
2230                if (query.length < 3) {
2231                  return false;
2232                }
2233                if(query){
2234                  query = query.replace(/^\s+|\s+$/gm, "");
2235                geocoder.geocode(query, function(results) {
2236
2237                  for (var i = 0; i < results.length; i++) {
2238                    var bbox = results[i].bbox || results[i].properties.bbox || null;
2239                    var bboxAttr = bbox ? ' data-bbox="'+JSON.stringify(bbox).replace(/"/g, '&quot;')+'"' : '';
2240
2241                    // Determine place type from result properties
2242                    var placeType = 'unknown';
2243                    if (results[i].properties) {
2244                      var osm_type = results[i].properties.type || results[i].properties.osm_type || '';
2245                      if (osm_type === 'administrative' || results[i].properties.class === 'boundary') {
2246                        if (results[i].properties.place_rank <= 4) placeType = 'country';
2247                        else if (results[i].properties.place_rank <= 12) placeType = 'region';
2248                        else placeType = 'city';
2249                      } else if (osm_type === 'city' || osm_type === 'town' || osm_type === 'village') {
2250                        placeType = 'city';
2251                      }
2252                    }
2253
2254                    output.push('<li data-latitude="'+results[i].center.lat+'" data-longitude="'+results[i].center.lng+'" data-place-type="'+placeType+'"'+bboxAttr+'>'+results[i].name+'</li>');
2255                  }
2256                  output.push('<li class="powered-by-osm">Powered by <strong>OpenStreetMap</strong></li>');
2257                  $('#leaflet-geocode-cont ul').html(output);
2258                  var txt_to_hl = query.split(' ');
2259                  txt_to_hl.forEach(function (item) {
2260                    $('#leaflet-geocode-cont ul').highlight(item);
2261                  });
2262                  $('#autocomplete-container').addClass("osm-dropdown-active");
2263                  $("#leaflet-geocode-cont").addClass('active');
2264                  output = [];
2265                });
2266              }
2267            }
2268        });
2269         $("#listeo_core-search-form").on( "click", "#leaflet-geocode-cont ul li", function(e) {
2270            
2271            $("#location_search").val($(this).text());
2272            $("#leaflet-geocode-cont").removeClass('active');
2273            $('#autocomplete-container').removeClass("osm-dropdown-active");
2274            
2275            var newLatLng = new L.LatLng($(this).data('latitude'), $(this).data('longitude'));
2276
2277            // Capture viewport from OSM bounding box for viewport-based search
2278            var bboxData = $(this).data('bbox');
2279            var placeType = $(this).data('place-type') || 'unknown';
2280            var form = $('#listeo_core-search-form');
2281
2282            // Remove old viewport fields
2283            form.find('input[name^="place_viewport"]').remove();
2284            form.find('input[name="place_type"]').remove();
2285
2286            // Handle bbox data - Leaflet Control Geocoder returns LatLngBounds object
2287            if (bboxData) {
2288              var south, north, west, east;
2289
2290              // Check if it's a Leaflet LatLngBounds object (has getSouthWest/getNorthEast methods)
2291              if (typeof bboxData.getSouthWest === 'function' && typeof bboxData.getNorthEast === 'function') {
2292                // Use Leaflet's public API
2293                var sw = bboxData.getSouthWest();
2294                var ne = bboxData.getNorthEast();
2295                south = sw.lat;
2296                west = sw.lng;
2297                north = ne.lat;
2298                east = ne.lng;
2299              }
2300              // Fallback: Check if it has private _southWest/_northEast properties
2301              else if (bboxData._southWest && bboxData._northEast) {
2302                south = bboxData._southWest.lat;
2303                west = bboxData._southWest.lng;
2304                north = bboxData._northEast.lat;
2305                east = bboxData._northEast.lng;
2306              }
2307              // Fallback: Check if it's an array (Nominatim format: [south, north, west, east])
2308              else if (Array.isArray(bboxData) && bboxData.length >= 4) {
2309                south = parseFloat(bboxData[0]);
2310                north = parseFloat(bboxData[1]);
2311                west = parseFloat(bboxData[2]);
2312                east = parseFloat(bboxData[3]);
2313              }
2314              // Fallback: Try parsing as JSON string
2315              else if (typeof bboxData === 'string') {
2316                try {
2317                  var parsed = JSON.parse(bboxData);
2318                  if (Array.isArray(parsed) && parsed.length >= 4) {
2319                    south = parseFloat(parsed[0]);
2320                    north = parseFloat(parsed[1]);
2321                    west = parseFloat(parsed[2]);
2322                    east = parseFloat(parsed[3]);
2323                  }
2324                } catch (e) {
2325                  console.warn('Listeo Viewport (OSM): Failed to parse bbox string', e);
2326                }
2327              }
2328
2329              // If we successfully extracted coordinates, add them to the form
2330              if (typeof south !== 'undefined' && typeof north !== 'undefined' &&
2331                  typeof west !== 'undefined' && typeof east !== 'undefined') {
2332
2333                // Convert to our viewport format (ne_lat, ne_lng, sw_lat, sw_lng)
2334                form.append('<input type="hidden" name="place_viewport[ne_lat]" value="' + north + '">');
2335                form.append('<input type="hidden" name="place_viewport[ne_lng]" value="' + east + '">');
2336                form.append('<input type="hidden" name="place_viewport[sw_lat]" value="' + south + '">');
2337                form.append('<input type="hidden" name="place_viewport[sw_lng]" value="' + west + '">');
2338              }
2339            }
2340
2341            // Add place type for auto mode
2342            form.append('<input type="hidden" name="place_type" value="' + placeType + '">');
2343
2344            // check if map exists
2345            if(window.map){
2346            // Only fly to location if autofit is disabled
2347            // When autofit is enabled, fitBounds() will handle positioning based on search results
2348            if(listeo_core.maps_autofit != 'on'){
2349              map.flyTo(newLatLng, 10);
2350            }
2351
2352            // Update user location marker immediately if radius is set
2353            var radiusValue = $('.distance-radius').val() || $('#search_radius').val();
2354            if (radiusValue && radiusValue > 0) {
2355              updateUserLocationOnMap(
2356                $(this).data('latitude'),
2357                $(this).data('longitude'),
2358                radiusValue,
2359                $(this).text()
2360              );
2361            }
2362            }
2363            var target   = $('div#listeo-listings-container' );
2364            target.triggerHandler( 'update_results', [ 1, false ] );
2365        });
2366
2367        $('#listeo_core-search-form').on('submit', function(){
2368            if ($('#location_search:focus').length){ return false;}
2369        });
2370
2371        if($("#location_search").val()) {
2372          var query = $("#location_search").val()
2373          query = query.replace(/^\s+|\s+$/gm, "");
2374          geocoder.geocode(query, function(results) {
2375
2376            // Only fly to search location if autofit is disabled
2377            // When autofit is enabled, fitBounds() handles the map positioning
2378            if(map && listeo_core.maps_autofit != 'on' && results && results.length && results[0] && results[0].center){
2379              map.flyTo(results[0].center, 10);
2380            }
2381
2382
2383          });
2384        }
2385
2386      
2387
2388      
2389      var mouse_is_inside = false;
2390
2391      $( "#location_search,#_address,#leaflet-geocode-cont" ).on( "mouseenter", function() {
2392          mouse_is_inside=true;
2393      });
2394      $( "#location_search,#_address,#leaflet-geocode-cont" ).on( "mouseleave", function() {
2395          mouse_is_inside=false;
2396      });
2397
2398      $("body").mouseup(function(){
2399          if(! mouse_is_inside) $("#leaflet-geocode-cont").removeClass('active');
2400      });
2401}
2402
2403      $(".geoLocation, #listeo_core-search-form .location a,.main-search-input-item.location a,.form-field-_address-container a").on("click", function (e, autoTriggered) {
2404          e.preventDefault();
2405
2406          geolocate(!!autoTriggered);
2407      });
2408
2409      function geolocate(silent) {
2410
2411          if (navigator.geolocation) {
2412              navigator.geolocation.getCurrentPosition(function (position) {
2413                  
2414                  var latitude = position.coords.latitude;
2415                  var longitude = position.coords.longitude;
2416                  var latlng = L.latLng(latitude, longitude);
2417
2418                  // Submit-listing map: reposition the draggable location pin.
2419                  // The submit map (submit_map / submit_marker) is a separate map
2420                  // from the search map (window.map) and has no listeners tying the
2421                  // coordinate fields to the pin, so the "Find my location" button
2422                  // used to write the coordinates without ever moving the marker.
2423                  // Runs before the address-provider branching so it works for both
2424                  // Google and OSM geocoding. Guarded so it is a no-op on any page
2425                  // without a submit map.
2426                  if (window.submit_marker && window.submit_map) {
2427                      $('#_geolocation_lat').val(latitude);
2428                      $('#_geolocation_long').val(longitude);
2429                      window.submit_marker.setLatLng(latlng).update();
2430                      window.submit_map.setView(latlng, 14);
2431                  }
2432
2433                  if(listeo_core.address_provider == 'google'){
2434                      if(window.map){
2435                        // Geolocate flyTo is programmatic: suppress map-move
2436                        // search and drop any previously injected bounds.
2437                        autoLocationMapMove = true;
2438                        ignoreMapEvents = true;
2439                        userMovedMap = false;
2440                        $('#listeo_core-search-form')
2441                          .find('.map-bounds, input[name="search_by_map_move"]')
2442                          .remove();
2443                        // Failsafe: if the flyTo animation is interrupted and
2444                        // moveend never fires, do not lock the flags forever.
2445                        // Tracked so a newer geolocate cycle cancels the stale
2446                        // timer instead of being disarmed by it mid-flight.
2447                        clearTimeout(autoLocationFailsafeTimer);
2448                        autoLocationFailsafeTimer = setTimeout(function () {
2449                          autoLocationMapMove = false;
2450                          ignoreMapEvents = false;
2451                        }, 3000);
2452                        map.flyTo([latitude,longitude],map.getZoom());
2453                      }
2454                        $('#_geolocation_lat').val(latitude);
2455                        $('#_geolocation_long').val(longitude);
2456                      // Maps API loads with loading=async - the reverse
2457                      // geocode must wait for it if the user clicked
2458                      // geolocate before the API finished loading.
2459                      var reverseGeocode = function() {
2460                      var pos = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);
2461                      var geocoder = new google.maps.Geocoder();
2462                      geocoder.geocode( { 'latLng': pos}, function(results, status) {
2463                        if (status == google.maps.GeocoderStatus.OK) {
2464                          if (results[1]) {
2465                            if($('#location_search').length){
2466                              if($('#location_search').val().length === 0 ) {
2467                                $('#location_search').val(results[1].formatted_address);
2468                              }
2469                            }
2470                             $("#_address").val(results[1].formatted_address);
2471                          }
2472
2473                    }
2474                  });
2475                      };
2476                      whenGoogleMapsReady(reverseGeocode);
2477
2478                  } else {
2479                      if(listeo_core.address_provider == 'osm'){
2480                        if(listeo_core.country){
2481                          var geocoder = new L.Control.Geocoder.Nominatim( {
2482                              geocodingQueryParams: {countrycodes: listeo_core.country} //accept-language: 'en'
2483                          });
2484                        } else {
2485                          var geocoder = new L.Control.Geocoder.Nominatim();  
2486                        } 
2487                      }
2488                    // Nominatim resolves the reverse geocode at the zoom we pass:
2489                    // ~9 returns a county/region name ("O Condado, Pontevedra"),
2490                    // 10 returns the municipality ("Salvaterra de Miño, ...").
2491                    // The zoom used to be taken from map.getZoom(), which during
2492                    // the (animated, still pending) flyTo is the archive map's
2493                    // initial zoom - default 9 - so "Find my location" filled the
2494                    // search field with a REGION instead of the user's town,
2495                    // unlike the Google address provider which is always precise.
2496                    // Fixed zoom 10 = city/municipality granularity, the right
2497                    // scope for a location search field, independent of however
2498                    // far the visible map happens to be zoomed out.
2499                    var reverseScale = 262144; // 256 * 2^10 -> Nominatim zoom 10
2500                    if(window.map){
2501                      // Geolocate flyTo is programmatic: suppress map-move
2502                      // search and drop any previously injected bounds.
2503                      autoLocationMapMove = true;
2504                      ignoreMapEvents = true;
2505                      userMovedMap = false;
2506                      $('#listeo_core-search-form')
2507                        .find('.map-bounds, input[name="search_by_map_move"]')
2508                        .remove();
2509                      // Failsafe: if the flyTo animation is interrupted and
2510                      // moveend never fires, do not lock the flags forever.
2511                      // Tracked so a newer geolocate cycle cancels the stale
2512                      // timer instead of being disarmed by it mid-flight.
2513                      clearTimeout(autoLocationFailsafeTimer);
2514                      autoLocationFailsafeTimer = setTimeout(function () {
2515                        autoLocationMapMove = false;
2516                        ignoreMapEvents = false;
2517                      }, 3000);
2518                      map.flyTo([latitude,longitude],10);
2519                      geocoder.reverse(latlng, reverseScale, function(results) {
2520                        var resultName = (results && results.length && results[0] && results[0].name) ? results[0].name : '';
2521                        if($('#location_search').length){
2522                          if($('#location_search').val().length === 0 && resultName) {
2523                            $("#location_search").val(resultName);
2524                          }
2525                        }
2526
2527                        if (resultName) {
2528                          $("#_address").val(resultName);
2529                        }
2530                        $('#_geolocation_lat').val(latitude);
2531                        $('#_geolocation_long').val(longitude);
2532                        var newLatLng = new L.LatLng(latitude, longitude);
2533                        marker.setLatLng(newLatLng).update();
2534                        map.panTo(newLatLng);
2535                        var listing_results      = $('#listeo-listings-container');
2536                        listing_results.triggerHandler( 'update_results', [ 1, false ] );
2537                      });
2538                    } else {
2539                      geocoder.reverse(latlng, reverseScale, function (results) {
2540                        var resultName = (results && results.length && results[0] && results[0].name) ? results[0].name : '';
2541                        if ($("#location_search").length) {
2542                          if ($("#location_search").val().length === 0 && resultName) {
2543                            $("#location_search").val(resultName);
2544                          }
2545                        }
2546                        if (resultName) {
2547                          $("#_address").val(resultName);
2548                        }
2549                        $("#_geolocation_lat").val(latitude);
2550                        $("#_geolocation_long").val(longitude);
2551                        var listing_results = $("#listeo-listings-container");
2552                        listing_results.triggerHandler("update_results", [
2553                          1,
2554                          false,
2555                        ]);
2556                      });
2557
2558                    }
2559
2560                }
2561
2562              }, function () {
2563                  // Geolocation denied, unavailable, or timed out. Without this
2564                  // callback nothing happened at all - the map silently stayed
2565                  // at the site's configured default center, which customers
2566                  // read as "my location was detected somewhere else" (e.g. a
2567                  // pin sitting on the default-center city). Tell the visitor
2568                  // what happened instead — but only for a MANUAL click on the
2569                  // button. The maps_autolocate page-load trigger passes
2570                  // silent=true so visitors who denied geolocation aren't
2571                  // alerted on every page view.
2572                  if (silent) { return; }
2573                  var msg = (window.listeo_core && listeo_core.geolocation_failed)
2574                      ? listeo_core.geolocation_failed
2575                      : 'We could not detect your location. Please allow location access in your browser, or type your location manually.';
2576                  window.alert(msg);
2577              });
2578          }
2579      }
2580
2581      // Add event handler for radius changes to update user location circle in real time
2582      $(document).on('input change', '.distance-radius, #search_radius', function() {
2583        var radiusValue = $(this).val();
2584        var locationSearch = $('#location_search').val();
2585        
2586        if (window.map && locationSearch && radiusValue && radiusValue > 0) {
2587          // Update the radius circle if user location is already shown
2588          if (userLocationMarker && radiusCircle) {
2589            var lat = userLocationMarker.getLatLng().lat;
2590            var lng = userLocationMarker.getLatLng().lng;
2591            updateUserLocationOnMap(lat, lng, radiusValue, locationSearch);
2592          }
2593        }
2594      });
2595    
2596      if(listeo_core.maps_autolocate){
2597        // The extra `true` reaches the click handler as its second argument
2598        // (autoTriggered) so the auto-locate on page load stays SILENT when
2599        // geolocation is denied/unavailable — only a manual click on the
2600        // "Find my location" button shows the failure alert.
2601        $(".geoLocation, #listeo_core-search-form .location a,.main-search-input-item.location a").trigger('click', [true])
2602      }
2603  });
2604})(this.jQuery);
2605
2606jQuery.fn.highlight = function(pat) {
2607 function innerHighlight(node, pat) {
2608  var skip = 0;
2609  if (node.nodeType == 3) {
2610   var pos = node.data.toUpperCase().indexOf(pat);
2611   if (pos >= 0) {
2612    var spannode = document.createElement('span');
2613    spannode.className = 'highlight';
2614    var middlebit = node.splitText(pos);
2615    var endbit = middlebit.splitText(pat.length);
2616    var middleclone = middlebit.cloneNode(true);
2617    spannode.appendChild(middleclone);
2618    middlebit.parentNode.replaceChild(spannode, middlebit);
2619    skip = 1;
2620   }
2621  }
2622  else if (node.nodeType == 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) {
2623   for (var i = 0; i < node.childNodes.length; ++i) {
2624    i += innerHighlight(node.childNodes[i], pat);
2625   }
2626  }
2627  return skip;
2628 }
2629 return this.each(function() {
2630  innerHighlight(this, pat.toUpperCase());
2631 });
2632};
2633
2634jQuery.fn.removeHighlight = function() {
2635 function newNormalize(node) {
2636    for (var i = 0, children = node.childNodes, nodeCount = children.length; i < nodeCount; i++) {
2637        var child = children[i];
2638        if (child.nodeType == 1) {
2639            newNormalize(child);
2640            continue;
2641        }
2642        if (child.nodeType != 3) { continue; }
2643        var next = child.nextSibling;
2644        if (next == null || next.nodeType != 3) { continue; }
2645        var combined_text = child.nodeValue + next.nodeValue;
2646        new_node = node.ownerDocument.createTextNode(combined_text);
2647        node.insertBefore(new_node, child);
2648        node.removeChild(child);
2649        node.removeChild(next);
2650        i--;
2651        nodeCount--;
2652    }
2653 }
2654
2655 return this.find("span.highlight").each(function() {
2656    var thisParent = this.parentNode;
2657    thisParent.replaceChild(this.firstChild, this);
2658    newNormalize(thisParent);
2659 }).end();
2660};

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.