diff --git a/templates/index.html b/templates/index.html
index 603d93a..69fa606 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -648,57 +648,33 @@
MARKER STYLES
===================================================== */
- /* Wrapper that holds both the label and the pin circle */
- .poi-pin-wrapper {
- position: relative;
- cursor: pointer;
- width: 34px;
- height: 34px;
- }
-
- /* Permanent name label shown above each pin */
- .pin-label {
- position: absolute;
- bottom: 38px;
- left: 50%;
- transform: translateX(-50%);
-
- background: rgba(255, 255, 255, 0.92);
- border: none;
- border-radius: 4px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
- color: #202124;
- font-size: 12px;
- font-weight: 500;
- padding: 2px 7px;
- white-space: nowrap;
- cursor: pointer;
- }
-
- /* Custom circular pin that reflects the POI type */
- .poi-pin-inner {
- width: 34px;
- height: 34px;
-
+ /*
+ * Simple coloured dot marker.
+ * Kept intentionally minimal so MapLibre's internal
+ * transform-based positioning stays stable during
+ * zoom / pan animations on vector-tile maps.
+ */
+ .poi-dot {
+ width: 28px;
+ height: 28px;
border-radius: 50%;
-
+ border: 3px solid #fff;
+ box-shadow: 0 2px 6px rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: center;
-
- font-size: 17px;
- line-height: 1;
-
- color: white;
-
- border: 2px solid white;
-
- box-shadow:
- 0 2px 6px rgba(0, 0, 0, 0.35);
+ font-size: 14px;
+ cursor: pointer;
+ /* Prevent any CSS transition from fighting MapLibre's
+ per-frame transform updates during camera moves. */
+ will-change: transform;
}
+ .poi-dot.selected {
+ box-shadow: 0 0 0 4px rgba(26,115,232,0.35), 0 2px 8px rgba(0,0,0,0.5);
+ }
- /* User location marker (blue dot) */
+ /* User location marker (blue pulsing dot) */
.user-loc-marker {
cursor: default;
}
@@ -1020,17 +996,50 @@
);
- let markers = [];
+ /** All search-result POI markers currently on the map. */
+ let poiMarkers = [];
+ /** The single marker representing the user's current selection (click or search). */
let selectedMarker = null;
+ /** Popup attached to the selected marker (reverse-geocode result). */
let selectedPopup = null;
+ /** Blue dot showing the user's GPS position. */
let userLocationMarker = null;
let zoomMessageTimer = null;
+ /* =========================================================
+ MARKER FACTORY
+ ========================================================= */
+
+ /**
+ * Build a simple coloured dot element for a POI marker.
+ * Deliberately kept flat (no nested absolutely-positioned
+ * children, no SVG, no filters) so that MapLibre's per-frame
+ * transform updates stay rock-stable during zoom / pan on
+ * vector-tile maps.
+ */
+ function createDotElement(color, icon) {
+ const el = document.createElement("div");
+ el.className = "poi-dot";
+ el.style.background = color;
+ el.textContent = icon || "";
+ el.title = "";
+ return el;
+ }
+
+
+ /**
+ * Plain red dot for map-click selections / fallbacks.
+ */
+ function createDefaultDot() {
+ return createDotElement("#ea4335", "");
+ }
+
+
/* =========================================================
SEARCH
========================================================= */
@@ -1125,168 +1134,77 @@
resultsCard.innerHTML = "";
-
- const bounds = [];
-
+ const lngLats = [];
data.forEach(place => {
- const result =
- document.createElement(
- "div"
- );
-
-
- result.className =
- "result";
+ // Skip results with missing/invalid coordinates
+ if (
+ typeof place.lat !== "number" ||
+ typeof place.lon !== "number" ||
+ !isFinite(place.lat) ||
+ !isFinite(place.lon) ||
+ Math.abs(place.lat) > 90 ||
+ Math.abs(place.lon) > 180
+ )
+ return;
+ // --- Results list item ---
+ const result = document.createElement("div");
+ result.className = "result";
result.innerHTML = `
-
-
- ${getIcon(place)}
-
-
+ ${getIcon(place)}
-
-
- ${escapeHtml(
- getName(place)
- )}
-
-
-
- ${escapeHtml(
- getShortAddress(place)
- )}
-
-
-
- ${escapeHtml(
- formatType(
- getType(place)
- )
- )}
-
-
+
${escapeHtml(getName(place))}
+
${escapeHtml(getShortAddress(place))}
+
${escapeHtml(formatType(getType(place)))}
`;
+ result.addEventListener("click", () => {
+ selectPlace(place);
+ resultsCard.style.display = "none";
+ });
- result.addEventListener(
- "click",
- () => {
+ resultsCard.appendChild(result);
- selectPlace(
- place
- );
-
- resultsCard.style.display =
- "none";
- }
+ // --- Map marker (simple dot, centered on coordinate) ---
+ const dotEl = createDotElement(
+ getPoiColor(place),
+ getIcon(place)
);
+ dotEl.title = getName(place);
+ const marker = new maplibregl.Marker({
+ element: dotEl,
+ anchor: "center"
+ }).setLngLat([place.lon, place.lat]).addTo(map);
- resultsCard.appendChild(
- result
- );
-
-
- // Build the marker element with label + pin
- const el =
- document.createElement(
- "div"
- );
-
- el.className =
- "poi-pin-wrapper";
-
- el.innerHTML = `
-
- ${escapeHtml(
- getName(place)
- )}
-
-
- ${getIcon(place)}
-
- `;
-
-
- const marker =
- new maplibregl.Marker({
- element: el,
- anchor: "center"
- })
- .setLngLat([
- place.lon,
- place.lat
- ])
- .addTo(map);
-
-
- marker.getElement().addEventListener(
- "click",
- () => {
-
- selectPlace(
- place
- );
-
- resultsCard.style.display =
- "none";
- }
- );
-
-
- markers.push(marker);
-
-
- bounds.push([
- place.lat,
- place.lon
- ]);
+ // Stop propagation so clicking a POI pin does NOT
+ // also trigger the map's reverse-geocode handler.
+ marker.getElement().addEventListener("click", e => {
+ e.stopPropagation();
+ highlightPoiMarker(marker);
+ selectPlace(place);
+ resultsCard.style.display = "none";
+ });
+ poiMarkers.push(marker);
+ lngLats.push([place.lon, place.lat]);
});
-
- if (bounds.length === 1) {
-
- const zoom = getZoomForPlace(data[0]);
-
+ // Fly / fit to results
+ if (lngLats.length === 1) {
map.flyTo({
- center: [
- bounds[0][1],
- bounds[0][0]
- ],
- zoom: zoom,
+ center: lngLats[0],
+ zoom: getZoomForPlace(data[0]),
duration: 700
});
-
- }
- else {
-
- const lons =
- bounds.map(b => b[1]);
-
- const lats =
- bounds.map(b => b[0]);
-
- map.fitBounds([
- [
- Math.min(...lons),
- Math.min(...lats)
- ],
- [
- Math.max(...lons),
- Math.max(...lats)
- ]
- ], {
- padding: 80
- });
+ } else {
+ const sw = [Math.min(...lngLats.map(p => p[0])), Math.min(...lngLats.map(p => p[1]))];
+ const ne = [Math.max(...lngLats.map(p => p[0])), Math.max(...lngLats.map(p => p[1]))];
+ map.fitBounds([sw, ne], { padding: 80 });
}
}
@@ -1430,9 +1348,7 @@
let url =
"/search?q=" +
- encodeURIComponent(
- query + ", "
- );
+ encodeURIComponent(query);
if (viewbox) {
@@ -1515,40 +1431,46 @@
}
+ /**
+ * Highlight a POI marker by adding the "selected" class.
+ * Called when the user clicks on an existing search-result pin.
+ */
+ function highlightPoiMarker(marker) {
+ // Remove highlight from all other POI markers
+ poiMarkers.forEach(m => {
+ m.getElement().classList.remove("selected");
+ });
+ marker.getElement().classList.add("selected");
+ }
+
+
function selectPlace(place) {
- placeCard.style.display =
- "block";
-
- resultsCard.style.display =
- "none";
-
+ placeCard.style.display = "block";
+ resultsCard.style.display = "none";
clearSelection();
+ // Coloured dot for known POIs, red default otherwise
+ const isKnownPoi = place.kind === "poi" || place.kind === "address";
+ const dotEl = isKnownPoi
+ ? createDotElement(getPoiColor(place), getIcon(place))
+ : createDefaultDot();
+ dotEl.title = getName(place);
- selectedMarker =
- new maplibregl.Marker()
- .setLngLat([
- place.lon,
- place.lat
- ])
- .addTo(map);
-
+ selectedMarker = new maplibregl.Marker({
+ element: dotEl,
+ anchor: "center"
+ }).setLngLat([place.lon, place.lat]).addTo(map);
const zoom = getZoomForPlace(place);
-
map.flyTo({
- center: [
- place.lon,
- place.lat
- ],
+ center: [place.lon, place.lat],
zoom: zoom,
duration: 700
});
-
renderPlace(place);
}
@@ -1625,14 +1547,10 @@
clearSelection();
-
- selectedMarker =
- new maplibregl.Marker()
- .setLngLat([
- lon,
- lat
- ])
- .addTo(map);
+ selectedMarker = new maplibregl.Marker({
+ element: createDefaultDot(),
+ anchor: "center"
+ }).setLngLat([lon, lat]).addTo(map);
try {
@@ -2529,14 +2447,8 @@
function clearMarkers() {
- markers.forEach(
- marker =>
- marker.remove()
- );
-
-
- markers = [];
-
+ poiMarkers.forEach(marker => marker.remove());
+ poiMarkers = [];
clearSelection();
}