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

Geotools Laravel Package

micro-module/geotools

Geotools is a Laravel package for geographic data and geospatial helpers. Work with coordinates, distances, proximity calculations, and location formatting through convenient PHP APIs that make adding map-aware features to your application simple.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the canonical package (the repo is thephpleague/geotools; the Composer name is league/geotools):

composer require league/geotools

It requires PHP 7.3+ and pulls in willdurand/geocoder (v4) plus Symfony Console/Process for its CLI and batch worker. There is no Laravel auto-discovery, service provider, facade, or config file — you wire it yourself.

Look first at these classes:

  • League\Geotools\Geotools — entry point/facade for distance, geocoder, batch, and vertex helpers.
  • League\Geotools\Coordinate\Coordinate — validated [latitude, longitude] value object.
  • League\Geotools\Distance\Distance — unit conversion and flat/great-circle/Vincenty calculations.

The fastest first use case is a distance calculation, with no HTTP or API key required:

use League\Geotools\Geotools;
use League\Geotools\Coordinate\Coordinate;
use League\Geotools\Distance\Distance;

$geotools = new Geotools();

$paris  = new Coordinate([48.8234055, 2.3072664]);
$lyon   = new Coordinate([45.764043, 4.835659]);

$km = $geotools->distance()
    ->setFrom($paris)
    ->setTo($lyon)
    ->in(Distance::UNIT_KILOMETER)
    ->greatCircle(); // float, in kilometers because of in()

For address geocoding you also need a PSR-18 HTTP client and PSR-17 factory, because Geocoder v4 providers do not ship one:

composer require php-http/guzzle7-adapter nyholm/psr7
use Geocoder\ProviderAggregator;
use Geocoder\Provider\Nominatim\Nominatim;
use Http\Adapter\Guzzle7\Client as GuzzleAdapter;
use Nyholm\Psr7\Factory\Psr17Factory;

$http = new GuzzleAdapter(new \GuzzleHttp\Client(['timeout' => 5]));
$factory = new Psr17Factory();

$geocoder = new ProviderAggregator();
$geocoder->registerProvider(
    Nominatim::withOpenStreetMapServer($http, $factory, 'MyApp/1.0 ([email protected])')
);

$collection = $geotools->geocoder()
    ->using($geocoder)
    ->geocode('Paris, France')
    ->get();

$coordinate = $collection->isEmpty() ? null : $collection->first()->getCoordinates();

Implementation Patterns

Bind it once in the container

Create a small service provider so controllers/jobs never construct Geotools or providers directly:

// app/Providers/GeotoolsServiceProvider.php
namespace App\Providers;

use Geocoder\Provider\Nominatim\Nominatim;
use Geocoder\ProviderAggregator;
use Http\Adapter\Guzzle7\Client as GuzzleAdapter;
use Illuminate\Support\ServiceProvider;
use League\Geotools\Geotools;
use Nyholm\Psr7\Factory\Psr17Factory;

class GeotoolsServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(Geotools::class, fn () => new Geotools());

        $this->app->singleton(ProviderAggregator::class, function () {
            $aggregator = new ProviderAggregator();
            $http    = new GuzzleAdapter(new \GuzzleHttp\Client(['timeout' => 5]));
            $factory = new Psr17Factory();

            $aggregator->registerProvider(
                Nominatim::withOpenStreetMapServer(
                    $http,
                    $factory,
                    config('geotools.nominatim.user_agent')
                )
            );

            return $aggregator;
        });
    }
}
// config/geotools.php
return [
    'nominatim' => [
        'user_agent' => env('GEOCODER_USER_AGENT', 'MyApp/1.0 ([email protected])'),
    ],
    'cache_ttl' => 60 * 60 * 24 * 30,
];

Wrap it in one application service

Keep the third-party API behind your own class. This is your main extension point and makes testing trivial:

namespace App\Support;

use Geocoder\ProviderAggregator;
use League\Geotools\Coordinate\Coordinate;
use League\Geotools\Distance\Distance;
use League\Geotools\Geotools;

final class GeoService
{
    public function __construct(
        private Geotools $geotools,
        private ProviderAggregator $geocoder,
    ) {}

    public function distanceKm(Coordinate $from, Coordinate $to): float
    {
        return $this->geotools->distance()
            ->setFrom($from)
            ->setTo($to)
            ->in(Distance::UNIT_KILOMETER)
            ->greatCircle();
    }

    public function geocode(string $address): ?Coordinate
    {
        $key = 'geocode:'.sha1(mb_strtolower(trim($address)));

        return \Cache::remember($key, config('geotools.cache_ttl'), function () use ($address) {
            $result = $this->geotools->geocoder()
                ->using($this->geocoder)
                ->geocode($address)
                ->get();

            if ($result->isEmpty()) {
                return null;
            }

            $c = $result->first()->getCoordinates();

            return new Coordinate([$c->getLatitude(), $c->getLongitude()]);
        });
    }

    public function reverse(float $lat, float $lng): ?string
    {
        $result = $this->geotools->geocoder()
            ->using($this->geocoder)
            ->reverse($lat, $lng)
            ->get();

        return $result->isEmpty() ? null : $result->first()->getStreetName();
    }
}

Note that Coordinate is a value object; if you cache it directly, confirm serialization works in your cache driver. Caching the [lat, lng] array or returning Coordinate from the closure both work, but the latter is easier at call sites.

Pick the right distance algorithm

$distance = $geotools->distance()->setFrom($a)->setTo($b);

$distance->in(Distance::UNIT_METER)->flat();        // planar approximation, small areas only
$distance->in(Distance::UNIT_KILOMETER)->greatCircle(); // haversine, spherical
$distance->in(Distance::UNIT_MILE)->vincenty();     // ellipsoidal, slowest/most accurate

Every call to in() or flat() returns a value and the Distance object is stateful. Build a fresh distance() chain per calculation rather than reusing a Distance instance across requests or loop iterations.

Use Vertex for map/bearing work

$bearing = $geotools->vertex()
    ->setFrom($paris)
    ->setTo($lyon)
    ->initialBearing(); // degrees

This is useful for map arrow rotation, “as the crow flies” headings, and route previews. It uses great-circle math, not road routing.

Move batch geocoding into queued jobs

Batch is convenient for dozens of addresses, but it still performs network I/O. Dispatch a job, chunk the input, and rate-limit the provider:

namespace App\Jobs;

use App\Support\GeoService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\RateLimiter;

class GeocodeAddresses implements ShouldQueue
{
    use Dispatchable, Queueable;

    public function __construct(public array $addresses) {}

    public function handle(GeoService $geo): void
    {
        foreach (array_chunk($this->addresses, 25) as $chunk) {
            foreach ($chunk as $address) {
                RateLimiter::attempt('nominatim', 1, function () use ($geo, $address) {
                    $geo->geocode($address);
                }, 1); // one per second
            }
        }
    }
}

If you use Batch::parallel(), understand that it dispatches work to child processes via the CLI. That is often fine on a worker, but it is a poor fit for web requests and can fail where proc_open is disabled. Prefer queued chunked jobs unless you specifically need the batch result aggregation.

Store coordinates with sensible precision

Use decimal(10,7) for latitude and decimal(10,7) for longitude (roughly centimeter precision), and cast to float in Eloquent:

protected $casts = [
    'latitude'  => 'float',
    'longitude' => 'float',
];

For “find nearby” queries, do not load every row and call Distance in PHP. Add a bounding-box prefilter and use MySQL’s ST_Distance_Sphere (MySQL 5.7+/MariaDB) or PostGIS. Use Geotools’ distance methods for calculations already in PHP or for small candidate sets.

Test with a fake geocoder

Because GeoService depends on Geocoder\Geocoder/ProviderAggregator, you can swap in a fake implementation in tests. Test the distance logic with fixed coordinates and never hit Nominatim in CI.

Gotchas and Tips

  • Coordinate order is [latitude, longitude]. GeoJSON, Google Maps URLs, and many APIs are [longitude, latitude]. Passing [-73.98, 40.75] (NYC in GeoJSON order) is silently accepted as an odd but valid coordinate in some cases and throws in others. Always construct new Coordinate([$lat, $lng]) deliberately. Coordinate validates ranges (-90..90, -180..180), so out-of-range values throw InvalidArgumentException.
  • flat() is not the earth. It uses a planar approximation and drifts quickly beyond a few kilometers. Use greatCircle() for general distance and vincenty() only when ellipsoidal accuracy matters.
  • vincenty() can fail for near-antipodal points where the iterative solution does not converge. Have a fallback to greatCircle().
  • The Distance object is mutable/stateful. Code like $d = $geotools->distance()->setFrom($a)->setTo($b); reused across loop iterations will surprise you. Rebuild each time.
  • Geocoder v4 needs an HTTP client and message factory. Without php-http/guzzle7-adapter (or another PSR-18 adapter and PSR-17 factory), provider construction fails or requests fail with discovery errors. This is the most common installation problem.
  • Nominatim has usage rules. Set a descriptive User-Agent with contact info, stay near one request per second, and cache aggressively. Google, Mapbox, and other providers require API keys and billing; put keys in config/services.php, not in package config.
  • Always check isEmpty(). $collection->first() can throw when no result is found, and rate-limit or quota responses may surface as exceptions rather than empty collections. Catch provider exceptions and decide whether to retry or return null.
  • Batch::parallel() needs a working process environment. It spawns subprocesses; shared hosting, some containers, and disable_functions=proc_open setups will break it. Queues plus chunking are usually the safer Laravel answer.
  • The CLI binary (vendor/bin/geotools) is Symfony Console based. It is handy for one-off checks (distance:flat, geocoder:geocode, vertex:initial-bearing) but not necessary in production. Custom Artisan commands calling GeoService give you better logging and config reuse.
  • Ellipsoid selection lives on the coordinate. If you need non-WGS84 results, set it explicitly ($coordinate->setEllipsoid(...)) and keep it consistent across both points. Distances use the from-coordinate’s ellipsoid.
  • Units are easy to mix up. Distance::UNIT_METER, UNIT_KILOMETER, UNIT_MILE, UNIT_FOOT, UNIT_NAUTICAL_MILE. Call in() before the algorithm method, otherwise you get the default unit (meters).
  • Cache negative results carefully. Do not cache null for a month if the address was merely temporarily un-geocodable; use a short TTL for misses and a long TTL for hits.
  • Wrap, do not scatter. The package has no Laravel facade for a reason: provider construction, HTTP clients, caching, and rate limiting are application concerns. GeoService is the seam for swapping providers, adding metrics, and faking results in tests.
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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo