> ## Documentation Index
> Fetch the complete documentation index at: https://docs.loqate.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Store Finder Implementation Guide

> Add a store locator to your website with copy-paste code examples

export const GistEmbed = ({url, height = "500px"}) => {
  const [htmlContent, setHtmlContent] = useState('');
  const [copied, setCopied] = useState(false);
  const [rawCode, setRawCode] = useState('');
  const [isDark, setIsDark] = useState(true);
  useEffect(() => {
    const checkTheme = () => {
      const htmlElement = document.documentElement;
      const computedStyle = window.getComputedStyle(htmlElement);
      const colorScheme = computedStyle.getPropertyValue('color-scheme');
      const isDarkMode = htmlElement.classList.contains('dark') || htmlElement.getAttribute('data-theme') === 'dark' || colorScheme.includes('dark') || window.matchMedia('(prefers-color-scheme: dark)').matches;
      setIsDark(isDarkMode);
    };
    checkTheme();
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    mediaQuery.addEventListener('change', checkTheme);
    const observer = new MutationObserver(checkTheme);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['class', 'data-theme', 'style']
    });
    return () => {
      mediaQuery.removeEventListener('change', checkTheme);
      observer.disconnect();
    };
  }, []);
  useEffect(() => {
    fetch(url).then(r => r.text()).then(code => {
      setRawCode(code);
      const bgColor = isDark ? '#0a0c0e' : '#ffffff';
      const textColor = isDark ? '#c9d1d9' : '#24292e';
      const scrollTrack = isDark ? '#161b22' : '#f6f8fa';
      const scrollThumb = isDark ? '#30363d' : '#d1d5da';
      const scrollThumbHover = isDark ? '#484f58' : '#959da5';
      const iframe = `
          <!DOCTYPE html>
          <html>
            <head>
              <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
              <style>
                body {
                  margin: 0;
                  padding: 0;
                  background: ${bgColor};
                  overflow: auto;
                }
                pre {
                  margin: 0;
                  background: transparent !important;
                }
                code {
                  font-family: 'JetBrains Mono', JetBrains Mono, monospace !important;
                  font-size: 14px !important;
                  line-height: 1.7 !important;
                  background: transparent !important;
                  padding: 0 !important;
                }
                .hljs {
                  background: transparent !important;
                  color: ${textColor} !important;
                }
                ::-webkit-scrollbar {
                  width: 12px;
                  height: 12px;
                }
                ::-webkit-scrollbar-track {
                  background: ${scrollTrack};
                  border-radius: 10px;
                }
                ::-webkit-scrollbar-thumb {
                  background: ${scrollThumb};
                  border-radius: 10px;
                  border: 2px solid ${scrollTrack};
                }
                ::-webkit-scrollbar-thumb:hover {
                  background: ${scrollThumbHover};
                }
                * {
                  scrollbar-width: thin;
                  scrollbar-color: ${scrollThumb} ${scrollTrack};
                }
              </style>
            </head>
            <body>
              <pre><code class="language-html">${escapeHtml(code)}</code></pre>
              <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
              <script>hljs.highlightAll();</script>
            </body>
          </html>
        `;
      setHtmlContent(iframe);
    }).catch(e => {
      setRawCode('Error loading code');
      setHtmlContent('<pre>Error loading code</pre>');
    });
  }, [url, isDark]);
  const escapeHtml = text => {
    return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
  };
  const copyToClipboard = () => {
    navigator.clipboard.writeText(rawCode);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };
  const containerBg = isDark ? '#0a0c0e' : '#ffffff';
  const containerBorder = isDark ? '#30363d' : '#e1e4e8';
  return <div style={{
    position: 'relative',
    marginBottom: '1.5rem',
    marginTop: '1.5rem',
    background: containerBg,
    borderRadius: '12px',
    border: `1px solid ${containerBorder}`,
    padding: '16px 20px'
  }}>
      <button onClick={copyToClipboard} style={{
    position: 'absolute',
    top: '12px',
    right: '32px',
    padding: '8px',
    background: 'rgba(255, 255, 255, 0.08)',
    backdropFilter: 'blur(4px)',
    border: '1px solid rgba(255, 255, 255, 0.1)',
    borderRadius: '6px',
    cursor: 'pointer',
    fontSize: '13px',
    fontWeight: '500',
    color: 'rgba(255, 255, 255, 0.8)',
    zIndex: 10,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    transition: 'all 0.15s',
    fontFamily: 'system-ui, -apple-system, sans-serif',
    width: '32px',
    height: '32px'
  }} onMouseEnter={e => {
    e.currentTarget.style.background = 'rgba(255, 255, 255, 0.12)';
  }} onMouseLeave={e => {
    e.currentTarget.style.background = 'rgba(255, 255, 255, 0.08)';
  }}>
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          {copied ? <polyline points="20 6 9 17 4 12" /> : <>
              <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
              <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
            </>}
        </svg>
      </button>
      <iframe srcDoc={htmlContent} style={{
    width: '100%',
    height: height,
    border: 'none',
    overflow: 'auto',
    background: 'transparent'
  }} title="Code Example" />
    </div>;
};

**Help customers find your nearest locations with distance calculations and interactive maps.**

## Who Should Use This Guide

**For:** Business owners and developers adding store locator functionality without complex setup.

**You'll need:** A Loqate API key, store locations with addresses or coordinates, and basic HTML knowledge.

**Not covered:** Tag-based integrations or advanced map customizations.

## What You'll Build

A store locator that:

* Finds nearest stores based on user's location or search
* Calculates road distances and travel times
* Displays results on an interactive map
* Shows store details (address, hours, phone)
* Works globally with your store locations

## Quick Start

To set up Store Finder, you'll need an API key. Find out how to get yours [here](/loqate-basics/create-an-api-key/).

**Note:** Store Finder requires two keys (Service Key and Management Key). These are automatically generated when you create a Store Finder service in your account.

### Upload Your Store Locations

**Option A: Via Account UI**

1. Go to [account.loqate.com](https://account.loqate.com/account#/Setup/)
2. Create a Location List
3. Upload CSV with your stores (address, name, phone, hours, etc.)
4. Enable "Geocode Address" if you don't have coordinates

**Option B: Via API**
Use the [Create List API](/api-reference/store-finder/location-management-create-list) to upload programmatically.

### Add Your Keys

Replace `YOUR_SERVICE_KEY` and `YOUR_LOCATION_LIST_ID` in the code with your actual keys.

### Test

1. Open your page
2. Enter a location like "Boston, MA" or allow browser geolocation
3. See nearest stores with distances
4. Click markers to see store details

<Note>
  Store Finder calculates road-based distances (not straight-line). Results include drive time estimates based on typical road speeds.
</Note>

***

## Sample Project: Retail Store Locator

This walkthrough shows how to build a complete store locator with map. Follow along to see it built step-by-step.

<Note>
  This is a detailed walkthrough. If you just want working code, skip to [Complete Store Locator Example](#complete-store-locator-example).
</Note>

### What We're Building

A store locator that:

* Accepts user location input or uses browser geolocation
* Calls Distance Finder API to find nearest stores
* Displays results in a list with distances and drive times
* Shows stores on an interactive map with clickable markers
* Responsive design that works on mobile

***

## How Store Finder Works

Store Finder uses a three-step process:

### 1. Get User Location

**Option A: Address search**

* User types city, ZIP code, or address
* Use Geocoding API for suggestions
* Get coordinates for selected location

**Option B: Browser geolocation**

* Request user's current position
* Get coordinates directly from browser

### 2. Find Nearest Stores

**API:** Distance Finder

* Send user's coordinates + your Location List ID
* API calculates road distances to all stores
* Returns nearest stores sorted by distance
* Includes drive time estimates

**Key parameters:**

* `LocationListId` - Your uploaded store list
* `OriginLatitude` / `OriginLongitude` - User's location
* `MaxResults` - How many stores to return (default 10)
* `MaxDistance` - Search radius in km (default 100km)

### 3. Display Results

**List view:**

* Show store names, addresses, distances
* Display drive time estimates
* Include phone numbers, hours, etc.

**Map view:**

* Plot stores as markers
* Center map on user location
* Click markers for store details
* Use Mapping API for map tiles

***

## Complete Store Locator Example

Here's a production-ready store locator with map, search, and geolocation:

<GistEmbed url="https://raw.githubusercontent.com/loqate/javascript-docs-sample/refs/heads/main/store-finder/complete-store-finder.html" height="700px" />

**Features:**

* Address search with geocoding
* Browser geolocation ("Use My Location" button)
* List view with distances and drive times
* Interactive map with store markers
* Store detail popups on marker click
* Responsive mobile-friendly design

**To use:**

1. Copy the code
2. Replace `YOUR_SERVICE_KEY` and `YOUR_LOCATION_LIST_ID`
3. Save as an HTML file
4. Open in a browser to test
5. Customize styling to match your brand

<Note>
  This example uses MapLibre GL JS for map rendering. The map library is loaded from CDN and requires no additional setup.
</Note>

<Note>
  This example searches for the 10 nearest stores within 100km. Adjust `MaxResults` and `MaxDistance` parameters based on your needs.
</Note>

## Code on GitHub

If you'd like to browse the code snippets in this implementation guide or clone them, visit the repository on [GitHub](https://github.com/loqate/javascript-docs-sample/tree/main/store-finder).

***

## Integration Tips

### Adding to Your Existing Site

To add store locator to your current website:

1. **Add the map library** to your page's `<head>` section
   * MapLibre GL JS (recommended)
   * Or Leaflet / Tangram

2. **Add the locator functions** from the integration example to your page's `<script>` section
   * `getMapTiles()` for map rendering
   * `findNearestStores()` for distance calculations
   * `displayResults()` for showing stores

3. **Add the HTML structure** for search input, results list, and map container
   * Search input field
   * Results container div
   * Map container div

4. **Add the CSS styles** from the integration example
   * Map dimensions and positioning
   * Results list styling
   * Mobile responsive styles

Reference the [complete example](#complete-store-locator-example) above for all the code you need.

### Managing Store Locations

**Updating store lists:**

**Via Account UI:**

* Edit locations individually
* Bulk upload new CSV (replaces entire list)
* Delete individual locations

**Via API:**

```javascript theme={null}
// Update a single store
await fetch(
    'https://api.addressy.com/DistanceFinder/Interactive/UpdateListOrPoint/v1.00/json6.ws',
    {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            Key: managementKey,
            ListId: locationListId,
            LocationId: storeId,
            Name: 'Updated Store Name',
            Latitude: 42.3601,
            Longitude: -71.0589
        })
    }
);
```

**Tip:** Use Management Key (not Service Key) for Create/Update/Delete operations.

### Search Radius and Performance

**Default values:**

* `MaxDistance`: 100km
* `MaxResults`: 10 stores

**Optimization tips:**

**Smaller radius = faster:**

```javascript theme={null}
// Urban area - smaller radius
MaxDistance: 25 // 25km

// Rural area - larger radius
MaxDistance: 200 // 200km
```

**Fewer results = faster:**

```javascript theme={null}
// Show top 5 nearest
MaxResults: 5

// Show up to 20 stores
MaxResults: 20
```

**Regional lists:**
For multi-country sites, create separate lists per region:

* `locations_us` - US stores
* `locations_uk` - UK stores
* `locations_eu` - European stores

This improves performance vs. one global list.

### Geocoding User Input

**Option 1: Global Geocoding (Recommended)**
Direct address to coordinates - include country for best results:

```javascript theme={null}
async function geocodeAddress(address) {
    const response = await fetch(
        `https://api.addressy.com/LocationServices/Geocoding/Global/v1.10/json6.ws?` +
        `Input=${encodeURIComponent(address)}&Key=${serviceKey}`
    );
    
    const data = await response.json();
    // Response: [{ Input: "...", Results: [{ Latitude, Longitude, ... }] }]
    return data[0].Results[0]; // Returns lat/long
}
```

<Note>
  **Important:** For best results, include country in searches (e.g., "London, UK" not just "London"). Ambiguous locations may return no coordinates.
</Note>

### Browser Geolocation

Request user's current location:

```javascript theme={null}
function getUserLocation() {
    return new Promise((resolve, reject) => {
        if (!navigator.geolocation) {
            reject(new Error('Geolocation not supported'));
            return;
        }
        
        navigator.geolocation.getCurrentPosition(
            position => {
                resolve({
                    latitude: position.coords.latitude,
                    longitude: position.coords.longitude
                });
            },
            error => {
                reject(error);
            },
            {
                enableHighAccuracy: true,
                timeout: 10000,
                maximumAge: 0
            }
        );
    });
}
```

**Note:** Browser geolocation requires HTTPS (except localhost).

### Displaying on Map

**Step 1: Get map tiles**

```javascript theme={null}
async function getMapTiles() {
    const response = await fetch(
        `https://api.addressy.com/LocationServices/Mapping/GetUrl/v1.00/json6.ws?` +
        `Key=${serviceKey}`
    );
    
    const data = await response.json();
    return data.VectorStyleUrl; // Signed tile URL (valid 1 hour)
}
```

**Step 2: Initialize map**

```javascript theme={null}
const tileUrl = await getMapTiles();

const map = new maplibregl.Map({
    container: 'map',
    style: tileUrl, // Use VectorStyleUrl directly
    center: [userLongitude, userLatitude],
    zoom: 12
});
```

**Step 3: Add store markers**

```javascript theme={null}
stores.forEach(store => {
    new maplibregl.Marker()
        .setLngLat([store.Longitude, store.Latitude])
        .setPopup(
            new maplibregl.Popup().setHTML(`
                <h3>${store.Name}</h3>
                <p>${store.Address}</p>
                <p>${store.Distance.toFixed(1)} km away</p>
            `)
        )
        .addTo(map);
});
```

### Store Data Structure

Include these fields when creating your location list:

**Required:**

* `Name` - Store name
* `Latitude` - Coordinate (if you have it)
* `Longitude` - Coordinate (if you have it)
* `Address` - Full address (if no coordinates)

**Optional but recommended:**

* `Address1`, `Address2` - Street address
* `City`, `State`, `PostalCode` - Location details
* `Phone` - Contact number
* `Hours` - Opening hours
* `Services` - Available services
* `CustomField1-5` - Any additional data

**Example:**

```json theme={null}
{
    "Name": "Downtown Store",
    "Address1": "123 Main Street",
    "City": "Boston",
    "State": "MA",
    "PostalCode": "02129",
    "Phone": "(617) 555-0123",
    "Hours": "Mon-Sat 9am-9pm, Sun 10am-6pm",
    "Latitude": 42.3601,
    "Longitude": -71.0589
}
```

### Styling Customization

Customize the store locator to match your design:

**Map styling:**

```css theme={null}
#map {
    height: 500px;
    width: 100%;
    border-radius: 8px;
}

/* Mobile adjustments */
@media (max-width: 768px) {
    #map {
        height: 300px;
    }
}
```

**Results list:**

```css theme={null}
.store-item {
    padding: 15px;
    border: 1px solid #ddd;
    border-radius: 4px;
    margin-bottom: 10px;
    cursor: pointer;
}

.store-item:hover {
    background: #f5f5f5;
    border-color: #4CAF50;
}

.store-distance {
    color: #666;
    font-size: 14px;
}
```

**Custom map markers:**

```javascript theme={null}
const el = document.createElement('div');
el.className = 'custom-marker';
el.style.backgroundImage = 'url(/path/to/marker.png)';
el.style.width = '30px';
el.style.height = '40px';

new maplibregl.Marker({ element: el })
    .setLngLat([longitude, latitude])
    .addTo(map);
```

***

## Troubleshooting

### Invalid API Key

Verify you're using the **Service Key** (not Management Key) for Distance Finder and Mapping API calls. Management Key is only for Create/Update/Delete operations.

### No Stores Found

**Check:**

* Location List ID is correct
* `MaxDistance` radius is large enough
* User's location is within range of your stores
* Location list has coordinates (not just addresses)

### Map Not Loading

**Check:**

* Map tile URL is recent (expires after 1 hour)
* MapLibre GL JS library is loaded
* Map container has defined height in CSS
* Browser console for JavaScript errors

### Distances Seem Wrong

Distance Finder calculates **road-based distances** (not straight-line). Drive time estimates assume typical road speeds. Results may vary from GPS navigation due to:

* Real-time traffic conditions
* Route preferences
* Updated road networks

### Geolocation Not Working

**Check:**

* Site is served over HTTPS (required except localhost)
* User granted location permission
* Browser supports geolocation API
* Timeout values are reasonable (10 seconds recommended)

### Performance Issues

**Optimize by:**

* Reducing `MaxResults` (fewer stores returned)
* Reducing `MaxDistance` (smaller search radius)
* Splitting stores into regional lists
* Caching map tile URLs (valid 1 hour)

***

## Data Privacy

* Store locations stored in Loqate systems
* User searches logged for 30 days for operational purposes
* Geolocation data not stored (processed in browser only)
* See [Privacy Policy](https://www.loqate.com/en-gb/legal/privacy-policy/) for complete details

***

## Support

* [Create List Endpoint API Reference](/api-reference/store-finder/location-management-create-list)
* [Distance Finder Endpoint API Reference](/api-reference/store-finder/global-distance-finder)
* [Mapping Endpoint API Reference](/api-reference/store-finder/mapping)
* [Contact](https://www.loqate.com/en-gb/contact/customer-support/)

***

## Related

* [Store Finder Overview](/our-services/store-finder/overview)
* [API Security](/loqate-basics/api-security)
* [Pricing](https://www.loqate.com/en-gb/pricing/)
