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>