Delivery Address Validation

This example demonstrates how to validate delivery addresses against service areas, calculate distances, and display dynamic delivery fees based on zones. Perfect for food delivery, e-commerce, and service providers.

Live Demo

Select a restaurant location, then enter a delivery address to see real-time validation with delivery zones, fees, and estimated times.

Delivery Zones

Express Zone
Up to 5km
Free delivery
20-30 mins
Standard Zone
Up to 10km
£2.99 fee
40-60 mins
Min. order: £15
Extended Zone
Up to 15km
£4.99 fee
60-90 mins
Min. order: £25
powered by
powered by Google

How It Works

This example demonstrates a complete delivery validation system using distance-based zone checking and dynamic fee calculation.

  1. Origin-Based Search: The autocomplete is configured with the restaurant/warehouse location as the origin parameter, showing distances from that point in the suggestions.
  2. Location Bias: Results are biased towards a 20km radius around the selected location using locationBias, making nearby addresses appear first.
  3. Distance Calculation: The Haversine formula calculates the precise distance between the restaurant and delivery address using latitude/longitude coordinates from the location field.
  4. Zone Validation: The distance is checked against predefined delivery zones (Express, Standard, Extended) to determine if delivery is available and calculate the appropriate fee and time.
  5. Dynamic Updates: When switching restaurant locations, the setRequestParams() method updates the search origin without reinitialising the entire autocomplete instance.
  6. Practical Applications: This pattern is ideal for food delivery apps, e-commerce with local delivery, service providers with coverage areas, and any business with distance-based pricing.
<script>
import { PlacesAutocomplete } from 'places-autocomplete-js';

document.addEventListener('DOMContentLoaded', () => {
  let selectedLocation = 'london';
  let autocomplete;

  // Restaurant/warehouse locations
  const locations = [
    { 
      id: 'london', 
      name: 'London Central Kitchen', 
      coords: { lat: 51.5074, lng: -0.1278 }
    },
    { 
      id: 'manchester', 
      name: 'Manchester Depot', 
      coords: { lat: 53.4808, lng: -2.2426 }
    },
    { 
      id: 'birmingham', 
      name: 'Birmingham Hub', 
      coords: { lat: 52.4862, lng: -1.8904 }
    }
  ];

  // Delivery zones configuration
  const deliveryZones = [
    { 
      name: 'Express Zone', 
      maxDistance: 5, 
      fee: 0, 
      time: '20-30 mins', 
      minOrder: 0 
    },
    { 
      name: 'Standard Zone', 
      maxDistance: 10, 
      fee: 2.99, 
      time: '40-60 mins', 
      minOrder: 15 
    },
    { 
      name: 'Extended Zone', 
      maxDistance: 15, 
      fee: 4.99, 
      time: '60-90 mins', 
      minOrder: 25 
    }
  ];

  // Calculate distance using Haversine formula
  function calculateDistance(point1, point2) {
    const R = 6371; // Earth's radius in km
    const dLat = toRad(point2.lat - point1.lat);
    const dLon = toRad(point2.lng - point1.lng);
    const lat1 = toRad(point1.lat);
    const lat2 = toRad(point2.lat);

    const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
              Math.sin(dLon/2) * Math.sin(dLon/2) * 
              Math.cos(lat1) * Math.cos(lat2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    return R * c;
  }

  function toRad(degrees) {
    return degrees * Math.PI / 180;
  }

  // Validate delivery address against zones
  function validateDeliveryAddress(placeLocation, originLocation) {
    const distance = calculateDistance(originLocation, placeLocation);
    
    for (const zone of deliveryZones) {
      if (distance <= zone.maxDistance) {
        return {
          delivers: true,
          zone: zone.name,
          distance: distance,
          fee: zone.fee,
          time: zone.time,
          minOrder: zone.minOrder,
          message: zone.fee === 0 
            ? 'Free delivery available!' 
            : `Delivery available for £${zone.fee.toFixed(2)}`
        };
      }
    }

    return {
      delivers: false,
      zone: null,
      distance: distance,
      fee: null,
      time: null,
      minOrder: null,
      message: 'Sorry, this address is outside our delivery area'
    };
  }

  // Get initial location
  const initialLocation = locations.find(loc => loc.id === selectedLocation);

  // Initialise Places Autocomplete
  autocomplete = new PlacesAutocomplete({
    containerId: 'delivery-autocomplete-container',
    googleMapsApiKey: 'YOUR_GOOGLE_MAPS_API_KEY',
    onResponse: (placeDetails) => {
      console.log('Address Selected:', placeDetails);
      
      if (placeDetails.location) {
        const location = locations.find(loc => loc.id === selectedLocation);
        if (location) {
          const validationResult = validateDeliveryAddress(
            placeDetails.location,
            location.coords
          );
          
          // Display validation result
          displayValidationResult(placeDetails, validationResult);
        }
      }
    },
    onError: (error) => {
      console.error('Autocomplete Error:', error.message || error);
    },
    requestParams: {
      region: 'GB',
      includedRegionCodes: ['GB'],
      origin: initialLocation.coords,
      locationBias:initialLocation.coords
    },
    fetchFields: [
      'formattedAddress',
      'location',
      'addressComponents'
    ],
    options: {
      placeholder: 'Enter your delivery address...',
      clear_input: false,
      distance: true,
      distance_units: 'km'
    }
  });

  // Create location selector buttons
  const locationContainer = document.getElementById('location-selector');
  locations.forEach(location => {
    const button = document.createElement('button');
    button.textContent = location.name;
    button.classList.add('location-button');
    if (location.id === selectedLocation) {
      button.classList.add('active');
    }
    
    button.addEventListener('click', () => {
      selectedLocation = location.id;
      
      // Update active button styling
      document.querySelectorAll('.location-button').forEach(btn => {
        btn.classList.remove('active');
      });
      button.classList.add('active');
      
      // Update autocomplete origin
      autocomplete.setRequestParams({
        origin: location.coords,
        locationBias:location.coords
      });
      
      // Clear input and results
      autocomplete.clear();
      document.getElementById('validation-result').innerHTML = '';
    });
    
    locationContainer.appendChild(button);
  });

  function displayValidationResult(placeDetails, result) {
    const container = document.getElementById('validation-result');
    
    let html = `
      <div class="validation-card">
        <div class="validation-header">
          <div class="status-icon ${result.delivers ? 'success' : 'error'}">
            ${result.delivers ? '✓' : '✗'}
          </div>
          <div>
            <h3>${result.delivers ? 'Delivery Available' : 'Outside Delivery Area'}</h3>
            <p class="address">${placeDetails.formattedAddress}</p>
          </div>
          ${result.zone ? `<span class="zone-badge">${result.zone}</span>` : ''}
        </div>
        
        ${result.delivers ? `
          <div class="delivery-details">
            <div class="detail-item">
              <span class="icon">📍</span>
              <div>
                <div class="label">Distance</div>
                <div class="value">${result.distance.toFixed(1)} km</div>
              </div>
            </div>
            
            <div class="detail-item">
              <span class="icon">🕐</span>
              <div>
                <div class="label">Estimated Time</div>
                <div class="value">${result.time}</div>
              </div>
            </div>
            
            <div class="detail-item">
              <span class="icon">🚚</span>
              <div>
                <div class="label">Delivery Fee</div>
                <div class="value">${result.fee === 0 ? 'FREE' : '£' + result.fee.toFixed(2)}</div>
              </div>
            </div>
            
            <div class="detail-item">
              <span class="icon">💷</span>
              <div>
                <div class="label">Minimum Order</div>
                <div class="value">${result.minOrder === 0 ? 'None' : '£' + result.minOrder.toFixed(2)}</div>
              </div>
            </div>
          </div>
          
          <div class="success-message">
            ✓ ${result.message}
          </div>
        ` : `
          <div class="delivery-details">
            <div class="detail-item">
              <span class="icon">📍</span>
              <div>
                <div class="label">Distance</div>
                <div class="value">${result.distance.toFixed(1)} km</div>
                <div class="note">Maximum delivery distance: ${deliveryZones[deliveryZones.length - 1].maxDistance} km</div>
              </div>
            </div>
          </div>
          
          <div class="error-message">
            ✗ ${result.message}
          </div>
        `}
      </div>
    `;
    
    container.innerHTML = html;
  }
});
</script>

<!-- Location Selector -->
<div id="location-selector" class="location-selector">
  <!-- Buttons will be dynamically created here -->
</div>

<!-- Delivery Zones Info (Optional) -->
<div class="delivery-zones">
  <h3>Delivery Zones</h3>
  <div class="zones-grid">
    <div class="zone-card express">
      <div class="zone-name">Express Zone</div>
      <div>Up to 5km</div>
      <div class="fee">Free delivery</div>
      <div>20-30 mins</div>
    </div>
    <div class="zone-card standard">
      <div class="zone-name">Standard Zone</div>
      <div>Up to 10km</div>
      <div class="fee">£2.99 fee</div>
      <div>40-60 mins</div>
      <div class="min-order">Min. order: £15</div>
    </div>
    <div class="zone-card extended">
      <div class="zone-name">Extended Zone</div>
      <div>Up to 15km</div>
      <div class="fee">£4.99 fee</div>
      <div>60-90 mins</div>
      <div class="min-order">Min. order: £25</div>
    </div>
  </div>
</div>

<!-- Address Autocomplete -->
<div id="delivery-autocomplete-container"></div>

<!-- Validation Result -->
<div id="validation-result"></div>

<style>
  .location-selector {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1.5rem;
  }
  
  .location-button {
    padding: 0.5rem 1rem;
    border: 1px solid #ccc;
    border-radius: 0.5rem;
    background: #f9fafb;
    cursor: pointer;
    transition: all 0.2s;
  }
  
  .location-button.active {
    background: #4f46e5;
    color: white;
    border-color: #4f46e5;
  }
  
  .delivery-zones {
    margin-bottom: 1.5rem;
    padding: 1rem;
    background: #f9fafb;
    border-radius: 0.5rem;
  }
  
  .zones-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1rem;
    margin-top: 1rem;
  }
  
  .zone-card {
    padding: 1rem;
    border-radius: 0.5rem;
    border: 2px solid;
    font-size: 0.875rem;
  }
  
  .zone-card.express {
    border-color: #10b981;
    background: #ecfdf5;
  }
  
  .zone-card.standard {
    border-color: #3b82f6;
    background: #eff6ff;
  }
  
  .zone-card.extended {
    border-color: #f59e0b;
    background: #fffbeb;
  }
  
  .zone-name {
    font-weight: 600;
    margin-bottom: 0.5rem;
  }
  
  .fee {
    font-weight: 600;
    color: #1f2937;
    margin: 0.5rem 0;
  }
  
  .min-order {
    font-size: 0.75rem;
    color: #6b7280;
  }
  
  .validation-card {
    margin-top: 1.5rem;
    padding: 1.5rem;
    border: 1px solid #e5e7eb;
    border-radius: 0.5rem;
    background: white;
  }
  
  .validation-header {
    display: flex;
    align-items: start;
    gap: 1rem;
    margin-bottom: 1rem;
  }
  
  .status-icon {
    width: 2.5rem;
    height: 2.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    font-size: 1.5rem;
    font-weight: bold;
  }
  
  .status-icon.success {
    background: #dcfce7;
    color: #166534;
  }
  
  .status-icon.error {
    background: #fee2e2;
    color: #991b1b;
  }
  
  .address {
    font-size: 0.875rem;
    color: #6b7280;
    margin-top: 0.25rem;
  }
  
  .zone-badge {
    padding: 0.5rem 1rem;
    background: #eef2ff;
    color: #4f46e5;
    border-radius: 0.5rem;
    font-size: 0.875rem;
    font-weight: 600;
  }
  
  .delivery-details {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 1.5rem;
    padding: 1.5rem 0;
    border-top: 1px solid #e5e7eb;
  }
  
  .detail-item {
    display: flex;
    gap: 0.75rem;
  }
  
  .detail-item .icon {
    font-size: 1.25rem;
  }
  
  .label {
    font-size: 0.75rem;
    color: #6b7280;
    text-transform: uppercase;
  }
  
  .value {
    font-size: 1.125rem;
    font-weight: 600;
    color: #1f2937;
    margin-top: 0.25rem;
  }
  
  .note {
    font-size: 0.75rem;
    color: #6b7280;
    margin-top: 0.25rem;
  }
  
  .success-message {
    padding: 1rem;
    background: #dcfce7;
    color: #166534;
    border-radius: 0.5rem;
    margin-top: 1rem;
  }
  
  .error-message {
    padding: 1rem;
    background: #fee2e2;
    color: #991b1b;
    border-radius: 0.5rem;
    margin-top: 1rem;
  }
</style>