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
How It Works
This example demonstrates a complete delivery validation system using distance-based zone checking and dynamic fee calculation.
- Origin-Based Search: The autocomplete is configured with the restaurant/warehouse location as the
originparameter, showing distances from that point in the suggestions. - Location Bias: Results are biased towards a 20km radius around the selected location using
locationBias, making nearby addresses appear first. - Distance Calculation: The Haversine formula calculates the precise distance between the restaurant and delivery address
using latitude/longitude coordinates from the
locationfield. - 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.
- Dynamic Updates: When switching restaurant locations, the
setRequestParams()method updates the search origin without reinitialising the entire autocomplete instance. - 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>