Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Geocode Laravel Package

jcf/geocode

Laravel package that automatically geocodes Eloquent models. Add a trait, define address fields, and jcf/geocode resolves latitude and longitude via Google Maps on save, with caching and configurable API credentials for location-aware apps and maps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up

composer require jcf/geocode
php artisan vendor:publish --tag=geocode   # or --provider="Jcf\Geocode\GeocodeServiceProvider"

The package is auto-discovered, so you get a service provider plus a facade alias out of the box. Your first three things to read, in order:

  1. config/geocode.php after publishing — this is where the API key and any default language/region/components live. The exact env var name is defined here; set it in .env (GOOGLE_MAPS_API_KEY or similar) and in config/services.php if you prefer to centralise credentials.
  2. The facade class (src/Facades/...) — it's a thin proxy, so the real public API is whatever the underlying class exposes. Confirm the exact method names for forward and reverse geocoding before you write anything.
  3. The README for the response shape: Google returns results[] with geometry.location.lat/lng, formatted_address, place_id, types, and location_type.

One prerequisite that trips everyone up: the key must be a server-side key with the Geocoding API enabled and billing active on the Google Cloud project. A browser/JS key will fail with REQUEST_DENIED and no useful message from the package.

First use case — the one you'll actually ship this week: an address input on a "find a store near me" page. Geocode the submitted address once, persist lat/lng alongside the address, then query your own stores table by distance. Do not geocode on every page view.

// routes/web.php or a controller
$point = \Geocode::geocode($request->validated('address')); // confirm method name

if (blank($point)) {
    return back()->withErrors(['address' => 'We could not locate that address.']);
}

Add decimal(10,7) columns for latitude/longitude and an index on them before you write the geocoding code — retrofitting the schema after you have rows is the annoying part.

Implementation Patterns

1. Hide the package behind your own interface. Adoption is low (0 dependents), so treat it as a swappable transport, not a foundation.

namespace App\Geo;

interface Geocoder
{
    public function forward(string $address): ?Coordinates;   // address -> lat/lng
    public function reverse(float $lat, float $lng): ?Address; // lat/lng -> address
}

Write one adapter class that calls the facade and maps the raw Google array into your own Coordinates/Address value objects. Everything else in the app depends on the interface. Swapping to Http::get('https://maps.googleapis.com/maps/api/geocode/json', ...) later becomes a one-file change.

2. Cache aggressively, keyed on a normalised address.

public function forward(string $address): ?Coordinates
{
    $key = 'geo:fwd:'.md5(mb_strtolower(preg_replace('/\s+/', ' ', trim($address))));

    return Cache::remember($key, now()->addDays(30), fn () => $this->callGoogle($address));
}

Normalising whitespace and case before hashing is what stops "10 Downing St" and "10 downing st " from burning two API calls. Cache negative results too, but for hours rather than days — typos are usually fixed within the same session.

3. Reverse geocode the browser's coordinates, not an address. The navigator.geolocation flow gives you lat/lng for free; reverse geocoding is how you turn that into "you're near Camden" for a heading. Round to 5–6 decimals before caching so nearby users share cache entries.

4. Model observer / saving hook for derived coordinates.

static::saving(function (self $model) {
    if ($model->isDirty('address') || blank($model->latitude)) {
        $point = app(Geocoder::class)->forward($model->address);
        $model->latitude  = $point?->lat;
        $model->longitude = $point?->lng;
    }
});

Keep it out of the request cycle for bulk work — see below.

5. Everything batch goes through a queued job. CSV imports and backfills will blow through quota and time out the request. One job per chunk of ~50, with ->delay() or a rate limiter between them, and $tries = 3 with a long backoff.

6. Proximity queries stay in SQL. Once you have coordinates, don't call Google again to "sort by distance" — use a bounding-box prefilter plus Haversine:

$rows = Store::query()
    ->whereBetween('latitude',  [$lat - $dLat, $lat + $dLat])
    ->whereBetween('longitude', [$lng - $dLng, $lng + $dLng])
    ->selectRaw('*, (6371 * acos(cos(radians(?)) * cos(radians(latitude))
        * cos(radians(longitude) - radians(?)) + sin(radians(?))
        * sin(radians(latitude)))) AS distance_km', [$lat, $lng, $lat])
    ->orderBy('distance_km')
    ->limit(20)
    ->get();

7. Testing. Never hit Google in tests. Bind a fake implementation of your Geocoder interface in the container, or if you must exercise the adapter itself, use Http::fake() and return a fixture JSON captured from a real successful response. Keep one fixture per interesting case: OK with multiple results, ZERO_RESULTS, and an error payload.

8. Store the place_id. It's the most durable identifier Google gives you and is the cheapest thing to re-resolve later if you need fresh coordinates.

Gotchas and Tips

  • ZERO_RESULTS is not an exception. An unmatched address comes back as a normal, empty result. Guard every call site with a null/empty check and a user-facing message, or you'll ship null into your distance maths and get 0.0 distances everywhere.
  • OVER_QUERY_LIMIT / REQUEST_DENIED are the ones that bite. They typically surface as a generic exception with a status buried in the payload. Log the whole response body on failure — the Google error_message field is what tells you whether it's billing, an unenabled API, or a missing key restriction.
  • Lat/lng order. Google returns location.lat and location.lng as strings in a nested array. Cast to (float) once, in one place, and never index by [0]/[1].
  • Ambiguity is your problem, not Google's. "Springfield" exists in dozens of countries. Always pass a components=country:GB|administrative_area:... filter (or region/language defaults in the config) for user-facing lookups. Also check geometry.location_type — prefer ROOFTOP/RANGE_INTERPOLATED and treat APPROXIMATE as low-confidence.
  • Google's ToS restrict caching. You may cache geocode results for a limited period (roughly 30 days) and should not permanently store coordinates as a substitute for geocoding. place_id is the exception and may be stored indefinitely. If your app is a "store addresses forever" design, cache coordinates for 30 days and re-resolve from the stored place_id on expiry.
  • No timeout control by default. The underlying HTTP call can hang a request for a long time. If the package exposes a Guzzle client or timeout setting, use it; if not, cap it at the job/queue level and never call it synchronously on a high-traffic page.
  • Quota is per-project, not per-key. A runaway loop in staging shares the same project quota as production unless you use separate Cloud projects. Separate projects per environment is cheap insurance.
  • Rate limit at 50 QPS and add usleep() between calls in any synchronous loop. A foreach over 200 addresses looks fine in dev and trips OVER_QUERY_LIMIT instantly in production.
  • Extension points are thin. There's very little to override — no driver abstraction, no response DTOs. The pragmatic extension point is your own adapter class: add retry-with-backoff, response normalisation, and metrics (count of calls, cache hit rate, error rate) there. Emitting a counter per call pays for itself the first time someone asks "why is the Google bill this high?"
  • Treat the package as unmaintained. Last release metadata aside, with 0 dependents and 48 stars, pin the version in composer.json and keep a Http:: fallback adapter in the same PR. The Google Geocoding REST endpoint is two query parameters and a JSON decode — the fallback is genuinely 20 lines.
  • Watch the config cache. After changing GEOCODE_API_KEY, run php artisan config:clear; a stale bootstrap/cache/config.php is the usual cause of "it works in tinker but not the web app."
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums