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 optionallylat/lng(usenullif the provider doesn’t return coordinates inline).placeDetails()— fetches coordinates by provider-specific ID. Returnnullwhen coordinates are already in thesearch()result — the widget treatsnullas “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_hereThen 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
- Start the backend:
php artisan serveindropafinder-app-backend/. - Open the Finder Builder at
http://localhost:3000. - Navigate to Search & Behavior (or equivalent) → Autocomplete provider.
- Select the new provider from the dropdown.
- Enter a test API key and save.
- Open a Finder embed (or use the Live Preview) and type in the search box — predictions should return from the new provider.
- 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 mainNo 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.providervalue, and the dashboardAutocompleteProvidertype union must all match exactly. A mismatch causes the controller to fall back to DropaSearch silently. - Not returning
nullfromplaceDetails()when lat/lng are insearch()— 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