Kak dobavit poiskovyk magazinov na luboy sayt bez Google Maps
Tutorials

Kak dobavit poiskovyk magazinov na luboy sayt bez Google Maps

Sozdayte polnostyu rabochiy poiskovik magazinov s poiskom, kartoy i panelyu spiska menee chem v 80 strokakh HTML i JavaScript, bez Google Maps, bez neozhidannykh platezhey.

MapAtlas Team10 min read
#store locator#maps api#google maps alternative#store finder#javascript maps#retail

Every retail brand, franchise, and service business eventually needs a store locator. It is the page customers visit when they already want to buy, they just need to know which branch to walk into. Getting that page wrong costs real conversions. Getting it right is simpler than most developers expect.

The standard approach has been Google Maps, but that comes with a cost structure that bites at scale. A busy retail site with 50,000 monthly visitors loading a maps page can rack up hundreds of euros in map load and geocoding charges overnight. There is no free tier for production use anymore, the billing is opaque, and the GDPR situation for EU businesses using a US-based mapping service adds compliance overhead on top.

This tutorial builds a complete store locator, interactive map, address search, synced list panel, marker clustering, and click-to-detail popups, using the MapAtlas Maps API and Geocoding API. No framework required. The full working example fits in roughly 80 lines of HTML and JavaScript. You can drop it into a WordPress custom HTML block, a Shopify section, or any CMS the same afternoon you read this.

By the end you will have:

  • A rendered vector map centred on your store network
  • Markers loaded from a JSON data array with clustering at low zoom
  • An address search bar powered by the Geocoding API
  • A synced list panel that highlights on map click
  • A mobile-responsive two-column layout

What a Store Locator Actually Needs

Before writing a line of code, it helps to be precise about the requirements. A working store locator has four moving parts:

  1. A map that renders tiles, accepts pan and zoom, and shows markers.
  2. A search input that geocodes the user's typed address to coordinates, then recentres the map.
  3. A marker layer that plots each store location, clusters nearby pins at low zoom, and opens a detail popup on click.
  4. A list panel that shows stores sorted by distance from the searched location, highlights the active store, and scrolls in sync with the map.

That is it. Every other feature, directions, opening hours, inventory stock, is an enhancement layered on top of these four. Build the core first.

Step 1: Load the MapAtlas SDK

The MapAtlas Maps API is compatible with the Mapbox GL JS interface, so any Mapbox GL JS tutorial or plugin works directly. Add the CDN links to your page <head>:

<link
  rel="stylesheet"
  href="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css"
/>
<script src="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.js"></script>

If you are using npm:

npm install @mapmetrics/mapmetrics-gl

Get your free API key at portal.mapmetrics.org/signup. One key covers map tiles, geocoding, and routing, no separate credentials to juggle.

Step 2: Define Your Store Data

Store data is just a GeoJSON FeatureCollection. Each feature carries the store's coordinates and whatever properties your popup needs: name, address, phone, opening hours.

const stores = {
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.9041, 52.3676] },
      properties: {
        id: 1,
        name: "Amsterdam Central",
        address: "Stationsplein 12, 1012 AB Amsterdam",
        phone: "+31 20 123 4567",
        hours: "Mon–Sat 09:00–20:00"
      }
    },
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.4777, 51.9244] },
      properties: {
        id: 2,
        name: "Rotterdam Lijnbaan",
        address: "Lijnbaan 10, 3012 EL Rotterdam",
        phone: "+31 10 987 6543",
        hours: "Mon–Sat 09:00–21:00"
      }
    },
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [5.1214, 52.0907] },
      properties: {
        id: 3,
        name: "Utrecht Centrum",
        address: "Oudegracht 45, 3511 AB Utrecht",
        phone: "+31 30 555 1234",
        hours: "Mon–Sun 10:00–19:00"
      }
    }
  ]
};

In production you would fetch this from an API endpoint or a CMS. The structure stays the same, the only difference is where the data originates.

Store locator map showing three branch pins across the Netherlands

[Image: Screenshot of the finished store locator with three pins on a vector map, a search bar at the top, and a list panel on the left showing branch names and addresses]

Step 3: Render the Map and Add Clustering

Initialize the map, add the store data as a GeoJSON source with clustering enabled, and paint the cluster circles and individual pin layer. Mapbox GL JS clustering is built into the source definition, no plugin required.

const map = new mapmetricsgl.Map({
  container: 'map',
  style: 'https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY',
  center: [5.2913, 52.1326], // Centre of the Netherlands
  zoom: 7
});

map.on('load', () => {
  // Add GeoJSON source with clustering
  map.addSource('stores', {
    type: 'geojson',
    data: stores,
    cluster: true,
    clusterMaxZoom: 12,
    clusterRadius: 50
  });

  // Cluster circles
  map.addLayer({
    id: 'clusters',
    type: 'circle',
    source: 'stores',
    filter: ['has', 'point_count'],
    paint: {
      'circle-color': '#3B82F6',
      'circle-radius': ['step', ['get', 'point_count'], 20, 10, 28, 30, 36]
    }
  });

  // Cluster count labels
  map.addLayer({
    id: 'cluster-count',
    type: 'symbol',
    source: 'stores',
    filter: ['has', 'point_count'],
    layout: {
      'text-field': '{point_count_abbreviated}',
      'text-size': 13
    },
    paint: { 'text-color': '#ffffff' }
  });

  // Individual store pins
  map.addLayer({
    id: 'unclustered-point',
    type: 'circle',
    source: 'stores',
    filter: ['!', ['has', 'point_count']],
    paint: {
      'circle-color': '#EF4444',
      'circle-radius': 8,
      'circle-stroke-width': 2,
      'circle-stroke-color': '#ffffff'
    }
  });
});

Clicking a cluster zooms in to reveal individual stores. Clicking an unclustered pin opens a popup.

Step 4: Wire Up Popups and the List Panel

When a user clicks a store pin, show a popup on the map and highlight the matching card in the list panel. Both interactions should be bidirectional, clicking a list card should also fly the map to that store.

// Click unclustered store → open popup + highlight list card
map.on('click', 'unclustered-point', (e) => {
  const { coordinates } = e.features[0].geometry;
  const { name, address, phone, hours, id } = e.features[0].properties;

  new mapmetricsgl.Popup()
    .setLngLat(coordinates)
    .setHTML(`
      <strong>${name}</strong>
      <p>${address}</p>
      <p>${phone}</p>
      <p>${hours}</p>
    `)
    .addTo(map);

  highlightCard(id);
});

// Click cluster → zoom in
map.on('click', 'clusters', (e) => {
  const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
  const clusterId = features[0].properties.cluster_id;
  map.getSource('stores').getClusterExpansionZoom(clusterId, (err, zoom) => {
    if (err) return;
    map.easeTo({ center: features[0].geometry.coordinates, zoom });
  });
});

function highlightCard(id) {
  document.querySelectorAll('.store-card').forEach(card => {
    card.classList.toggle('active', card.dataset.id === String(id));
  });
}

// Build list panel from store data
function buildListPanel() {
  const list = document.getElementById('store-list');
  stores.features.forEach(({ properties, geometry }) => {
    const card = document.createElement('div');
    card.className = 'store-card';
    card.dataset.id = properties.id;
    card.innerHTML = `
      <strong>${properties.name}</strong>
      <p>${properties.address}</p>
      <small>${properties.hours}</small>
    `;
    card.addEventListener('click', () => {
      map.flyTo({ center: geometry.coordinates, zoom: 14 });
      highlightCard(properties.id);
    });
    list.appendChild(card);
  });
}

Step 5: Add Address Search with the Geocoding API

The search bar takes a user's typed location, geocodes it via the Geocoding API, flies the map to that point, and re-sorts the list panel by distance.

async function searchLocation(query) {
  const url = new URL('https://api.mapatlas.eu/geocoding/v1/search');
  url.searchParams.set('text', query);
  url.searchParams.set('key', 'YOUR_API_KEY');

  const res = await fetch(url);
  const data = await res.json();

  if (!data.features.length) {
    alert('Address not found. Try a city or postcode.');
    return;
  }

  const [lng, lat] = data.features[0].geometry.coordinates;

  // Fly map to searched location
  map.flyTo({ center: [lng, lat], zoom: 10 });

  // Sort list by distance from searched point
  const sorted = [...stores.features].sort((a, b) => {
    const distA = haversine(lat, lng, a.geometry.coordinates[1], a.geometry.coordinates[0]);
    const distB = haversine(lat, lng, b.geometry.coordinates[1], b.geometry.coordinates[0]);
    return distA - distB;
  });

  document.getElementById('store-list').innerHTML = '';
  sorted.forEach(feature => {
    // Re-render each card (reuse buildListPanel logic)
  });
}

function haversine(lat1, lon1, lat2, lon2) {
  const R = 6371;
  const dLat = ((lat2 - lat1) * Math.PI) / 180;
  const dLon = ((lon2 - lon1) * Math.PI) / 180;
  const a =
    Math.sin(dLat / 2) ** 2 +
    Math.cos((lat1 * Math.PI) / 180) *
    Math.cos((lat2 * Math.PI) / 180) *
    Math.sin(dLon / 2) ** 2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

document.getElementById('search-btn').addEventListener('click', () => {
  const query = document.getElementById('search-input').value.trim();
  if (query) searchLocation(query);
});

Store locator search showing nearest branch results after postcode entry

[Image: The store locator after the user has typed a postcode into the search bar, the map has recentred and the list panel shows branches sorted by distance with the nearest at the top]

Step 6: Mobile-Responsive Layout

A store locator on mobile must stack vertically, map on top, list below, rather than side by side. Twenty lines of CSS handles this with a single media query breakpoint.

#locator-wrapper {
  display: flex;
  height: 600px;
  gap: 0;
}

#store-list {
  width: 300px;
  overflow-y: auto;
  border-right: 1px solid #e5e7eb;
  padding: 12px;
}

#map {
  flex: 1;
}

.store-card {
  padding: 12px;
  border-radius: 8px;
  cursor: pointer;
  margin-bottom: 8px;
  border: 2px solid transparent;
  transition: border-color 0.2s;
}

.store-card.active {
  border-color: #3B82F6;
  background: #EFF6FF;
}

@media (max-width: 640px) {
  #locator-wrapper {
    flex-direction: column;
    height: auto;
  }
  #store-list {
    width: 100%;
    border-right: none;
    border-top: 1px solid #e5e7eb;
    height: 280px;
  }
  #map {
    height: 350px;
  }
}

Billing and GDPR Comparison with Google Maps

If you have been running Google Maps on a retail site and wondering why this month's bill came in higher than expected, you are not alone. The Maps JavaScript API charges per map load. The Places API charges per autocomplete session and per geocoding request. Those costs compound fast. A site doing 50,000 visits a month, each loading the store locator page once, spends around €140/month on map loads alone before a single geocoding call.

MapAtlas uses flat monthly plans. There is no per-load or per-request charge that spikes without warning. You can read the full breakdown in Google Maps API Pricing in 2026: The True Cost Breakdown and the MapAtlas vs. Google Maps comparison.

For EU developers the GDPR angle matters too. Google Maps routes data through US infrastructure. MapAtlas is EU-hosted, ISO 27001 certified, and processes all requests within the EU. For retail businesses that are already managing customer consent carefully, using an EU-native mapping provider removes one more third-party transfer from your privacy policy.

Putting It All Together

The complete store locator, HTML structure, CSS layout, map init, clustering, popup handling, list panel, search, and distance sort, fits comfortably in one file. The structure looks like this:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Store Locator</title>
  <link rel="stylesheet"
    href="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css" />
  <script src="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.js"></script>
  <style>
    /* paste the CSS from Step 6 here */
  </style>
</head>
<body>
  <div id="search-bar">
    <input id="search-input" type="text" placeholder="Enter your city or postcode…" />
    <button id="search-btn">Search</button>
  </div>
  <div id="locator-wrapper">
    <div id="store-list"></div>
    <div id="map"></div>
  </div>
  <script>
    // paste store data, map init, clustering, popup, list panel, and search from Steps 2–5
  </script>
</body>
</html>

The result is a production-ready store locator with zero external dependencies beyond the MapAtlas SDK. There is no build step, no framework, and no ongoing billing surprises.

If you need to add routing, "get directions from my location to this store", the Routing API takes the user's coordinates and the store's coordinates and returns a full turn-by-turn route you can draw on the map as a line layer. The How to Add Interactive Maps to Your Website tutorial covers that next step in detail.

Next Steps

Frequently Asked Questions

Can I build a store locator without Google Maps?

Yes. MapAtlas provides a Mapbox GL JS-compatible Maps API and a Geocoding API that cover every feature a store locator needs, interactive map, address search, marker clustering, and popups, with no per-load billing and full GDPR compliance.

How much does a store locator cost to run on MapAtlas vs Google Maps?

MapAtlas is roughly 75% cheaper than Google Maps for equivalent usage. Google Maps charges per map load and per geocoding request, which adds up fast on a busy retail site. MapAtlas uses flat monthly plans with no per-request surprises.

Does MapAtlas work on WordPress and Shopify?

Yes. Because MapAtlas is pure JavaScript with no framework dependency, you can embed it in a WordPress custom HTML block, a Shopify theme section, or any CMS that lets you add a script tag and a div.

Оказалось полезным? Поделитесь.

Back to Blog