NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Coordinates carries a set of geofencing helpers. They are pure math — no provider, no network:

use RoundlyConsulting\Geolocation\DataTransferObjects\Coordinates;

$here = new Coordinates(48.1486, 17.1077);
$there = new Coordinates(48.21, 16.37);

$here->near($there, radiusKm: 100);         // bool — Haversine distance ≤ 100 km
$here->within([$a, $b, $c, $d]);            // point-in-polygon, bool
$here->bearingTo($there);                   // initial bearing, degrees 0–360 clockwise from north
$here->midpointTo($there);                  // Coordinates on the great circle
$box = $here->boundingBox(radiusKm: 10);    // BoundingBox
$box->contains($point);                     // bool

Method reference

MethodReturns
distanceTo(Coordinates): floatGreat-circle distance in metres (Haversine).
near(Coordinates, float $radiusKm): boolWhether the other point lies within the radius.
within(list<Coordinates>): boolPoint-in-polygon (ray casting); each edge runs the short way round, so polygons across the antimeridian work. Fewer than 3 vertices returns false; a polygon enclosing a pole is not supported.
bearingTo(Coordinates): floatInitial bearing, 0–360° clockwise from north.
midpointTo(Coordinates): CoordinatesGeographic midpoint along the great circle, longitude normalised into -180..180.
boundingBox(float $radiusKm): BoundingBoxThe smallest box holding every point within the radius. Wraps across ±180°; spans every longitude when the circle reaches a pole.
BoundingBox::contains(Coordinates): boolWhether a point falls inside the box, wrapped boxes included.
BoundingBox::crossesAntimeridian(): boolWhether the box wraps across ±180° (its south-west longitude is then greater than its north-east one).

Antimeridian and poles

The helpers work across the antimeridian and near the poles: midpointTo() normalises the longitude into -180..180, a bounding box that crosses ±180° wraps (crossesAntimeridian() is then true and its south-west longitude is greater than its north-east one), and a circle that reaches a pole spans every longitude. within() takes each polygon edge the shorter way round, so a polygon drawn across the antimeridian works. A polygon that encloses a pole is not supported.

Polygon zones

Describe a zone by its vertices — the ring doesn’t need to repeat the first point:

$deliveryZone = [
    new Coordinates(48.20, 17.00),
    new Coordinates(48.20, 17.20),
    new Coordinates(48.10, 17.20),
    new Coordinates(48.10, 17.00),   // no need to repeat the first vertex
];

$customer = new Coordinates(48.15, 17.11);

$customer->within($deliveryZone);   // true

Bounding boxes

A BoundingBox is an axis-aligned rectangle — ideal as a cheap SQL pre-filter before an exact distance check:

$box = (new Coordinates(48.1486, 17.1077))->boundingBox(radiusKm: 10);

$box->southWest;   // Coordinates — minimum latitude, western edge
$box->northEast;   // Coordinates — maximum latitude, eastern edge
$box->toArray();   // ['minLatitude' => ..., 'minLongitude' => ..., 'maxLatitude' => ..., 'maxLongitude' => ...]

// Near ±180° the box wraps: south-west longitude > north-east longitude
(new Coordinates(0.0, 179.99))->boundingBox(radiusKm: 10)->crossesAntimeridian();   // true

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.