These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

How to — add an autocomplete provider

Six autocomplete providers exist today: DropaSearch (built-in), Google Places, Radar, Mapbox, HERE, and Geoapify. Adding a seventh follows the same pattern used for all six.

The autocomplete path: widget calls GET /api/mapping/autocomplete?finder_token=…MappingController resolves the provider from the finder’s settings → AutocompleteProviderFactory instantiates the class → class calls the third-party API and normalizes results.

Step 1 — Create the provider class

Create a new class in dropafinder-app-backend/app/Services/Autocomplete/, following the AutocompleteProviderInterface:

// app/Services/Autocomplete/LocationIQProvider.php <?php namespace App\Services\Autocomplete; use Illuminate\Support\Facades\Http; class LocationIQProvider implements AutocompleteProviderInterface { public function __construct(private readonly string $apiKey) {} public function search(string $input, ?float $lat, ?float $lng, ?string $sessionToken): array { $params = [ 'key' => $this->apiKey, 'q' => $input, 'format' => 'json', 'limit' => 6, 'normalizecity' => 1, ]; if ($lat !== null && $lng !== null) { $params['lat'] = $lat; $params['lon'] = $lng; } $response = Http::timeout(5)->get( 'https://api.locationiq.com/v1/autocomplete', $params ); if (!$response->successful()) { return []; } $results = $response->json() ?? []; return array_map(fn(array $r) => [ 'id' => $r['osm_id'] ?? '', 'description' => $r['display_name'] ?? '', 'main_text' => $r['address']['name'] ?? ($r['display_name'] ?? ''), 'secondary_text' => $r['address']['city'] ?? '', 'lat' => isset($r['lat']) ? (float) $r['lat'] : null, 'lng' => isset($r['lon']) ? (float) $r['lon'] : null, ], $results); } public function placeDetails(string $placeId, ?string $sessionToken): ?array { // Return null if lat/lng are already provided in search() results. // The widget will skip a second round-trip in that case. return null; } }

Interface contract (from AutocompleteProviderInterface.php):

  • search() — returns an array of normalized predictions. Each prediction must have keys: id, description, main_text, secondary_text, and optionally lat/lng (use null if the provider doesn’t return coordinates inline).
  • placeDetails() — fetches coordinates by provider-specific ID. Return null when coordinates are already in the search() result — the widget treats null as “use inline coordinates, skip the round-trip.”

Use Http::timeout(5) for all outbound requests. On any non-successful response, return an empty array — never throw.

Step 2 — Register in the factory

Open dropafinder-app-backend/app/Services/Autocomplete/AutocompleteProviderFactory.php and add a case:

public static function make(string $provider, ?string $apiKey): AutocompleteProviderInterface { return match ($provider) { 'google' => new GoogleProvider($apiKey ?? ''), 'radar' => new RadarProvider($apiKey ?? ''), 'mapbox' => new MapboxProvider($apiKey ?? ''), 'here' => new HereProvider($apiKey ?? ''), 'geoapify' => new GeoapifyProvider($apiKey ?? ''), 'locationiq' => new LocationIQProvider($apiKey ?? ''), // ← add default => new DropaSearchProvider(), }; }

The slug you use here ('locationiq') is the canonical provider identifier. It must match exactly what the frontend stores in settings.autocompleteProvider and in the FinderIntegration.provider column.

Step 3 — Verify the FinderIntegration model

Open app/Models/FinderIntegration.php. The model stores the encrypted API key in encrypted_api_key and resolves it through getApiKeyAttribute(). No changes are needed to the model — it is provider-agnostic.

MappingController looks up the integration by finder_id and provider slug. As long as the slug matches the factory key, the controller resolves the key automatically.

Step 4 — Add to the dashboard autocomplete settings

Open dropafinder-app-nextjs/src/lib/autocompleteSettings.ts and add the new provider:

// Extend the type union export type AutocompleteProvider = | 'dropaSearch' | 'google' | 'radar' | 'mapbox' | 'here' | 'geoapify' | 'locationiq'; // ← add // Add to the options array (shown in the builder dropdown) export const AUTOCOMPLETE_PROVIDER_OPTIONS: Array<{ value: AutocompleteProvider; label: string; sublabel: string; }> = [ { value: 'dropaSearch', label: 'DropaSearch', sublabel: 'Free, built-in — no key required' }, { value: 'google', label: 'Google Places', sublabel: 'Use your own Google Cloud key' }, { value: 'radar', label: 'Radar.io', sublabel: 'Use your own Radar key' }, { value: 'mapbox', label: 'Mapbox', sublabel: 'Use your own Mapbox key' }, { value: 'here', label: 'HERE Geocoding', sublabel: 'Use your own HERE key' }, { value: 'geoapify', label: 'Geoapify', sublabel: 'Use your own Geoapify key' }, { value: 'locationiq', label: 'LocationIQ', sublabel: 'Use your own LocationIQ key' }, // ← add ]; // Update PROVIDER_REQUIRES_KEY export const PROVIDER_REQUIRES_KEY: Record<AutocompleteProvider, boolean> = { dropaSearch: false, google: true, radar: true, mapbox: true, here: true, geoapify: true, locationiq: true, // ← add };

The AutocompleteProviderControl in src/components/finders/v2/sections/SearchFiltersSection.tsx reads AUTOCOMPLETE_PROVIDER_OPTIONS directly — no changes to the component are required. The new provider appears in the dropdown automatically.

Step 5 — Add the env var (for local dev)

If you need to test the new provider in development without a per-finder API key, add a fallback env var to dropafinder-app-backend/.env:

AUTOCOMPLETE_LOCATIONIQ_KEY=your_dev_key_here

Then read it in the factory if no finder-specific key is present. This is optional — the factory falls back to DropaSearchProvider (built-in) for any finder without an integration record.

Step 6 — Verify locally

  1. Start the backend: php artisan serve in dropafinder-app-backend/.
  2. Open the Finder Builder at http://localhost:3000.
  3. Navigate to Search & Behavior (or equivalent) → Autocomplete provider.
  4. Select the new provider from the dropdown.
  5. Enter a test API key and save.
  6. Open a Finder embed (or use the Live Preview) and type in the search box — predictions should return from the new provider.
  7. Verify via curl:
curl "http://127.0.0.1:8000/api/mapping/autocomplete?input=Main+St&finder_token=YOUR_TOKEN"

Expected: {"predictions": [...]} with results normalized to {id, description, main_text, secondary_text}.

Step 7 — Deploy

Backend first (new provider class), then dashboard (new option in dropdown):

# 1. Backend cd "/Users/codydavis/Local Sites/dropafinder-app-backend" make deploy # 2. Dashboard cd "/Users/codydavis/Local Sites/dropafinder-app-nextjs" git push origin main

No widget deploy is needed — the widget calls the backend’s /api/mapping/autocomplete endpoint and is agnostic to which provider is used.

See deploy for the full ritual.

Common mistakes

  • Mismatched slug — the factory slug, the FinderIntegration.provider value, and the dashboard AutocompleteProvider type union must all match exactly. A mismatch causes the controller to fall back to DropaSearch silently.
  • Not returning null from placeDetails() when lat/lng are in search() — causes an unnecessary second API call from the widget for every address selection.
  • Not handling non-200 responses — throw-on-error breaks finder search for all users who configured the provider. Always return [] on failure.

Where to next

  • Customer-facing autocomplete provider guide: content/integrations/autocomplete-providers.md
  • Add a map provider (separate surface): add-map-provider
  • Backend feature recipes: add-feature-backend