alexpechkarev/google-geocoder
PHP client for the Google Maps Geocoding API. Convert addresses into latitude/longitude coordinates and reverse geocode coordinates back into addresses with a simple, framework-agnostic interface for adding location lookup to your apps.
Install and wire it up
composer require alexpechkarev/google-geocoder
php artisan vendor:publish --provider="Alexpechkarev\GoogleGeocoder\GoogleGeocoderServiceProvider"
On Laravel 5.5+ the service provider and facade are auto-discovered. On older apps, register manually in config/app.php:
'providers' => [
Alexpechkarev\GoogleGeocoder\GoogleGeocoderServiceProvider::class,
],
'aliases' => [
'Geocoder' => Alexpechkarev\GoogleGeocoder\GoogleGeocoderFacade::class,
],
Configure the key
The published config/google-geocoder.php holds the API key (and typically region/language). Point it at an env var and never commit the key:
// config/google-geocoder.php
'key' => env('GOOGLE_GEOCODER_KEY'),
GOOGLE_GEOCODER_KEY=your-server-side-key
Create the key in Google Cloud Console with the Geocoding API enabled and billing active, and restrict it by server IP (not HTTP referrer — this is a server-side call).
First use case — turn a form address into coordinates
use Alexpechkarev\GoogleGeocoder\GoogleGeocoderFacade as Geocoder;
$results = Geocoder::geocode('1600 Amphitheatre Parkway, Mountain View, CA')->get();
if ($results->isEmpty()) {
// ZERO_RESULTS — nothing matched
}
$first = $results->first();
$coords = $first->getCoordinates(); // ['lat' => 37.42..., 'lng' => -122.08...]
$formatted = $first->getFormattedAddress();
$placeId = $first->getPlaceId();
That single call — address in, lat/lng + canonical formatted address + place_id out — is the 90% use case: geocoding user-entered addresses before saving, and reverse-geocoding a stored point back to a display string. Read the package README next for the exact Result accessor names in the version you install; the wrappers are thin around Google's results array.
Wrap it in your own service, not the facade everywhere
The facade is convenient but leaks a third-party API surface across your controllers. Put one class between them — it gives you a single place to add caching, logging, retries, and a fake for tests.
class GeocodingService
{
public function coordinatesFor(string $address): ?array
{
$key = 'geocode:'.md5(strtolower(trim($address)));
return Cache::remember($key, now()->addDays(30), function () use ($address) {
$results = Geocoder::geocode($address)->get();
if ($results->isEmpty()) {
return null;
}
$geo = $results->first()->getCoordinates();
return ['lat' => $geo['lat'], 'lng' => $geo['lng']];
});
}
}
Geocoding results for a given address are effectively static — cache aggressively. This is the single biggest lever on cost and latency.
Reverse geocoding for display
$results = Geocoder::reverseGeocode($lat, $lng)->get();
$label = $results->first()?->getFormattedAddress();
Use this when you have coordinates (from a map picker, a mobile device, a stored point) and need a human-readable address. Reverse geocoding is much less precise than forward — expect a street or neighborhood, not a rooftop.
Persist place_id, not just lat/lng
Store all three: latitude, longitude, and place_id. Coordinates of the same address can drift as Google updates its data; place_id is the stable canonical identifier and is what you should use if you later adopt Places API or need to re-resolve. Also store location_type if you care about precision (see Gotchas).
Address component filters for structured lookups
When you have a partial address and want it scoped (e.g. postal code within a country), pass components rather than string-concatenating:
Geocoder::geocode('Main Street', 'country:US|postal_code:94105')->get();
This is more reliable than building "Main Street, 94105, US" and avoids ambiguous cross-border matches.
Batch geocoding belongs in a queue
Geocoding is network I/O with a hard rate limit. Never loop over hundreds of records synchronously in a request. Push a job per record (or per chunk) and let workers throttle:
class GeocodeAddress implements ShouldQueue
{
use InteractsWithQueue, Queueable;
public int $tries = 3;
public int $backoff = 30;
public function __construct(public int $addressId) {}
public function handle(GeocodingService $geocoder): void
{
// resolve + persist; release() on rate-limit errors
}
}
Redis::throttle() or a small usleep between calls in a chunked job keeps you under Google's per-second cap and avoids OVER_QUERY_LIMIT.
Validate on the edges, geocode in a service
Validate the raw input in a FormRequest (non-empty, sane length), then do the geocode in a service or observer — never in a controller and never in a model accessor that fires on every read.
Testing
Because everything funnels through your service, you can bind a fake in tests:
$this->instance(GeocodingService::class, new class extends GeocodingService {
public function coordinatesFor(string $address): ?array
{
return ['lat' => 1.0, 'lng' => 2.0];
}
});
Alternatively, Geocoder::shouldReceive(...) on the facade, but the service swap is cleaner and keeps tests offline.
The package is unmaintained (last release 2017). Treat this as a risk decision, not a detail. It targets Laravel 5.x and Guzzle 6-era dependencies. On Laravel 9/10/11 and PHP 8.x you may hit:
guzzlehttp/guzzle,Before committing, run composer require on your actual Laravel version in a scratch branch. If it installs, great — but consider forking and pinning, or wrapping the raw Google REST API in ~50 lines yourself (it is a GET to /maps/api/geocode/json). The abstraction this package gives is genuinely thin.
ZERO_RESULTS is not an exception — it's an empty collection. ->get() returns an empty Collection when nothing matched. Always check isEmpty() before ->first(). Distinguish "no match" from "API error" in your UI.
Google error statuses need handling. OVER_QUERY_LIMIT, REQUEST_DENIED, INVALID_REQUEST, and UNKNOWN_ERROR come back in the response body with an HTTP 200. Inspect the response status (the README documents the accessor, commonly isSuccess() or a status accessor) and translate:
REQUEST_DENIED → almost always a bad/restricted key or missing billing. Check this first when "nothing works."OVER_QUERY_LIMIT → back off and retry the job; don't hammer.INVALID_REQUEST → your input or component string is malformed.Coordinates are approximate — record location_type. Google returns ROOFTOP, RANGE_INTERPOLATED, GEOMETRIC_CENTER, or APPROXIMATE. An APPROXIMATE result can be a block or a suburb off. If precision matters (delivery radius, distance calculations), branch on this and flag low-confidence results for manual review. Also check partial_match — Google will happily return a "best effort" for an address it couldn't fully resolve.
Don't trust the returned formatted address as canonical input. getFormattedAddress() is Google's normalized string; storing it is fine for display, but re-geocoding it can return subtly different results. Always key your cache on the user's original input (normalized case/whitespace), not the formatted output.
Config caching bites here. php artisan config:cache bakes env() calls in. If you add the key to .env after caching, it won't take effect. Re-run config:cache after changing the key, and always reference the key via config(), not env(), inside runtime code.
Language and region are per-call, not global. Even if the config sets defaults, pass $language/$region explicitly when the request context differs (e.g. a UK user's address). Results genuinely differ by region bias — a query for "Springfield" is ambiguous across countries.
Rate limits are per-project, shared across every caller. Geocoding caps around 50 requests/second (varies). If you also call Google APIs elsewhere in the app, all traffic shares the quota. Centralize geocoding through your service so you have one throttle point.
HTTPS/cert issues on old stacks. On legacy PHP with outdated CA bundles, Guzzle can throw cURL SSL errors. If you see this, update ca-certificates on the host rather than disabling verification in the package config.
Extension point: the package constructs its own HTTP client internally. If you need retries, logging, or a custom transport, the practical extension point is your wrapper service — intercept before calling the facade, catch its exceptions, and handle backoff there. Forking to inject a Guzzle client is possible but rarely worth it for a package this thin; wrapping is the lower-maintenance path.
Have an exit plan. Given the staleness, keep geocoding behind your own interface (GeocodingService) so swapping to the direct Maps API, geocoder-php/google-maps-provider, or a different vendor is a one-class change rather than a grep across the codebase.
How can I help you explore Laravel packages today?