Feature Guide

Address Field with Service Area Validation

January 2026 · 10 min read

"Do you service my area?" This question comes up constantly for service businesses. Instead of back-and-forth emails, let customers type their address and get an instant answer. With Google Places autocomplete, automatic distance calculation, and built-in service area validation.

Address validation isn't just about checking if an address exists. For service businesses, it's about answering the real question: can you actually help this customer? A plumber in Brooklyn doesn't want leads from New Jersey. A mobile detailer with a 20-mile radius needs to know upfront if the job is too far.

FormTs has a built-in address field with Google Places autocomplete, distance calculation, and service area validation. This guide shows how to use it.

The Basic Address Field

The address field uses Google Places autocomplete, so users get suggestions as they type. No more typos, no more incomplete addresses.

form.addRow(row => {
  row.addAddress('customerAddress', {
    label: 'Your Address',
    placeholder: 'Start typing your address...',
    isRequired: true
  });
});

When a user selects an address, you get the full details: formatted address, coordinates (latitude/longitude), country, country code, and Google Place ID.

Country Restrictions

Limit address suggestions to specific countries. This is useful when you only serve certain regions - no point showing UK addresses to a business that only operates in the US.

form.addRow(row => {
  row.addAddress('serviceAddress', {
    label: 'Service Address',
    placeholder: 'Enter a US address...',
    restrictToCountries: ['US'],
    isRequired: true
  });
});

// Multiple countries
form.addRow(row => {
  row.addAddress('northAmericaAddress', {
    label: 'North America Address',
    restrictToCountries: ['US', 'CA', 'MX']
  });
});

Use ISO 3166-1 alpha-2 country codes: 'US' (United States), 'GB' (United Kingdom), 'CA' (Canada), 'DE' (Germany), etc.

Distance Calculation

To calculate distance, you need a reference point - typically your business location. Set it using referenceAddress with coordinates.

// Your business location
const businessHQ: AddressValue = {
  formattedAddress: '123 Main St, Boston, MA 02101, USA',
  coordinates: { lat: 42.3601, lng: -71.0589 }
};

form.addRow(row => {
  row.addAddress('serviceAddress', {
    label: 'Service Address',
    placeholder: 'Where should we provide service?',
    referenceAddress: businessHQ,
    showDistance: true,
    distanceUnit: 'miles',
    isRequired: true
  });
});

Once configured, the address field automatically calculates distance. Enable showDistance: true to display a distance badge on the field. You can also access the distance programmatically.

// Display calculated distance
form.addRow(row => {
  row.addTextPanel('distanceInfo', {
    computedValue: () => {
      const addr = form.address('serviceAddress');
      const distance = addr?.distance();

      if (distance == null) {
        return 'Enter an address to see the distance.';
      }

      return 'Distance from our office: ' + distance.toFixed(1) + ' miles';
    }
  });
});

The distance() method returns the distance in the configured unit (km or miles). Returns null if no address is selected or no reference is configured.

Service Area Validation

Here's where it gets useful. Set maxDistance to define your service area. The field will show a validation error if the customer is too far away.

const companyHQ: AddressValue = {
  formattedAddress: 'Empire State Building, New York, NY',
  coordinates: { lat: 40.748817, lng: -73.985428 }
};

form.addRow(row => {
  row.addAddress('cleaningAddress', {
    label: 'Address to Clean',
    referenceAddress: companyHQ,
    showDistance: true,
    maxDistance: 15,  // Service area: 15 miles
    distanceUnit: 'miles',
    isRequired: true
  });
});

// Dynamic service status
form.addRow(row => {
  row.addTextPanel('serviceStatus', {
    computedValue: () => {
      const addr = form.address('cleaningAddress');
      if (!addr?.value()) return '';

      if (addr.isWithinRange()) {
        return '✅ Great! We service your area.';
      } else {
        const dist = addr.distance()?.toFixed(1) || 'N/A';
        return '❌ Sorry, ' + dist + ' miles is outside our 15 mile service area.';
      }
    }
  });
});

Use isWithinRange() to check if the address is within your service area. This lets you conditionally show content, calculate prices, or display messages based on whether you can serve the customer.

Pro tip

Be helpful, not just restrictive. If you can't service an area, mention if there's a referral partner, a waitlist, or special arrangements available. "We don't service this area yet, but contact us for special requests" is better than just "Sorry, too far."

Distance-Based Pricing

Many service businesses charge travel fees for customers outside a free zone. Instead of surprising customers with fees later, show them upfront.

const companyHQ: AddressValue = {
  formattedAddress: 'Dallas, TX, USA',
  coordinates: { lat: 32.7767, lng: -96.7970 }
};

const travelFeePerMile = 2;
const freeRadius = 5;

form.addRow(row => {
  row.addAddress('serviceAddress', {
    label: 'Property Address',
    referenceAddress: companyHQ,
    showDistance: true,
    maxDistance: 25,
    distanceUnit: 'miles',
    isRequired: true
  });
});

// Calculate travel fee
form.addRow(row => {
  row.addPriceDisplay('travelFee', {
    label: 'Travel Fee',
    currency: '$',
    computedValue: () => {
      const addr = form.address('serviceAddress');
      const distance = addr?.distance();

      if (distance == null || !addr?.isWithinRange()) return null;

      if (distance <= freeRadius) return 0;
      return Math.round((distance - freeRadius) * travelFeePerMile);
    }
  });
});

The travel fee updates automatically as soon as the customer selects an address. Within 5 miles? Free. Further out? They see exactly what the fee will be.

Contextual Messages

Raw numbers aren't always the best way to communicate. A message that explains what the distance means for the customer is more helpful.

form.addRow(row => {
  row.addTextPanel('serviceAreaMessage', {
    computedValue: () => {
      const addr = form.address('serviceAddress');
      const distance = addr?.distance();

      if (distance == null) return '';

      if (distance <= 5) {
        return '✓ Great news! You\'re in our free delivery zone.';
      } else if (addr?.isWithinRange()) {
        const fee = Math.round((distance - 5) * 2);
        return '📍 Travel fee: $' + fee + ' (' + distance.toFixed(1) + ' miles)';
      } else {
        return '⚠️ Sorry, this location is outside our service area.';
      }
    },
    isVisible: () => !!form.address('serviceAddress')?.value()
  });
});

The message adapts based on distance: celebration for free zones, clear pricing for travel fee areas, and a polite rejection for out-of-area addresses.

Interactive Map

Enable showMap: true to display an interactive map below the address input. The map shows markers for both the reference location and the selected address, with a connecting line colored green (in range) or red (out of range).

form.addRow(row => {
  row.addAddress('sessionLocation', {
    label: 'Session Location',
    placeholder: 'Where should we meet?',
    referenceAddress: studioLocation,
    showDistance: true,
    showMap: true,  // Display interactive map
    maxDistance: 30,
    distanceUnit: 'miles',
    isRequired: true
  });
});

The map automatically zooms to show both locations when an address is selected. It's powered by Leaflet, so it's lightweight and mobile-friendly.

Try the address tutorial with live code examples.

Complete Example

Here's a full cleaning service quote calculator combining address validation with pricing based on square footage, frequency, and travel distance.

const companyHQ: AddressValue = {
  formattedAddress: 'Chicago, IL, USA',
  coordinates: { lat: 41.8781, lng: -87.6298 }
};

const basePricePerSqFt = 0.15;
const travelFeePerMile = 2;
const freeRadius = 5;

form.addRow(row => {
  row.addAddress('cleaningAddress', {
    label: 'Address to Clean',
    referenceAddress: companyHQ,
    showDistance: true,
    showMap: true,
    maxDistance: 25,
    distanceUnit: 'miles',
    restrictToCountries: ['US'],
    isRequired: true
  });
});

form.addRow(row => {
  row.addSlider('squareFeet', {
    label: 'Square Footage',
    min: 500, max: 5000, step: 100,
    defaultValue: 1500,
    unit: 'sq ft'
  });
});

form.addRow(row => {
  row.addDropdown('frequency', {
    label: 'Cleaning Frequency',
    options: [
      { id: 'once', name: 'One-time cleaning' },
      { id: 'weekly', name: 'Weekly (-15%)' },
      { id: 'biweekly', name: 'Bi-weekly (-10%)' }
    ],
    defaultValue: 'once'
  });
});

// Price calculation
form.addRow(row => {
  row.addPriceDisplay('totalPrice', {
    label: 'Estimated Price',
    variant: 'highlight',
    computedValue: () => {
      const addr = form.address('cleaningAddress');
      if (!addr?.value() || !addr.isWithinRange()) return null;

      const sqft = form.slider('squareFeet')?.value() || 1500;
      const frequency = form.dropdown('frequency')?.value() || 'once';
      const distance = addr.distance() || 0;

      let price = sqft * basePricePerSqFt;

      // Travel fee (over 5 miles)
      if (distance > freeRadius) {
        price += (distance - freeRadius) * travelFeePerMile;
      }

      // Frequency discount
      if (frequency === 'weekly') price *= 0.85;
      else if (frequency === 'biweekly') price *= 0.90;

      return Math.round(price);
    }
  });
});

// Service area message
form.addRow(row => {
  row.addTextPanel('serviceCheck', {
    computedValue: () => {
      const addr = form.address('cleaningAddress');
      if (!addr?.value()) return 'Enter your address to get a quote.';

      if (!addr.isWithinRange()) {
        return '❌ Sorry, we only service within 25 miles of Chicago.';
      }

      return '✅ We service your area! Distance: ' + addr.distance()?.toFixed(1) + ' miles';
    }
  });
});

This creates a smooth experience: the customer types their address, sees whether they're in the service area, and gets a quote that includes any applicable travel fee. If they're outside the service area, the price doesn't show - just a clear message explaining why.

Accessing Address Data

The AddressValue type gives you everything Google Places returns about the selected address.

const addr = form.address('customerAddress')?.value();

if (addr) {
  // Full formatted address
  const fullAddress = addr.formattedAddress;

  // Coordinates for mapping/routing
  const lat = addr.coordinates.lat;
  const lng = addr.coordinates.lng;

  // Country info (when available)
  const country = addr.country;       // "United States"
  const code = addr.countryCode;      // "US"

  // Google Place ID for additional lookups
  const placeId = addr.placeId;
}

Use this data for CRM integration, analytics, routing to the nearest branch, or any custom logic your business needs.

AddressConfig Reference

Quick reference for all address configuration options:

  • placeholder - Placeholder text for the input
  • restrictToCountries - Array of ISO country codes (e.g., ['US', 'CA'])
  • referenceAddress - Your business location for distance calculation
  • showDistance - Display distance badge (default: false)
  • maxDistance - Maximum allowed distance (enables validation)
  • distanceUnit - 'km' or 'miles' (default: 'km')
  • showMap - Display interactive map (default: false)

AddressTs Methods

Methods available on the address field instance:

  • value() - Returns the current AddressValue or null
  • distance() - Distance from reference in configured unit (or null)
  • isWithinRange() - Whether address is within maxDistance

Common Use Cases

Mobile Service Businesses

For mobile services (detailing, cleaning, repair), address validation determines whether you can take the job and what to charge for travel.

Delivery Services

Delivery businesses often have tiered pricing: free within 5 miles, flat fee for 5-10 miles, per-mile charge beyond that. The distance() method makes this easy to calculate.

Multi-Location Businesses

If you have multiple locations, you can use referenceAddress dynamically based on which branch is selected, or calculate distance to each and pick the nearest.

Common Questions

How accurate is the distance calculation?

FormTs calculates straight-line distance (as the crow flies) using the Haversine formula. Actual driving distance will be longer due to roads. For most service area purposes, straight-line distance with a buffer works fine. If you need actual driving distance, you'd need to integrate a routing API separately.

What if someone is just outside the boundary?

Consider setting maxDistance slightly higher than your actual limit, then using distance() to show different messages or fees for edge cases. For example, maxDistance: 30 but 'standard pricing' only within 25 miles, with a 'contact us' message for 25-30 miles.

Can I validate against irregular shapes instead of circles?

The built-in maxDistance validation uses circular radius. For irregular shapes (like zip code boundaries or custom polygons), you'd need to implement custom logic using the coordinates returned in AddressValue and check against your polygon data.

Does this require a Google Maps API key?

The address autocomplete uses Google Places API, which is included in FormTs. You don't need to configure a separate API key - it's handled automatically.

Ready to Add Address Validation?

Try the interactive tutorial with live code editing and preview.