Skip to content

Examples

These walkthroughs put the whole API to work on real problems. Each is self-contained and runnable — copy it, adjust the coordinates, and adapt it to your domain. They build on the Quick Start and use the singleton for convenience.

Full source in the repo

These are condensed from the package's examples/ directory, which has nine focused single-topic files plus four complete application examples. Browse them on GitHub.

Ride-sharing — driver matching & surge zones

A ride-sharing platform indexes drivers by their cell, then answers "find me the nearest drivers" by expanding a gridDisk around the rider. A coarser resolution buckets demand and supply into surge zones.

The core idea: keep a map from cell → [driver IDs]. When a ride comes in, look up the rider's cell, expand outward, and collect every driver in the covered cells.

driver-matching.php
use Foysal50x\H3\H3;
 
$h3 = H3::getInstance();
 
const DRIVER_RES = 9;   // ~174 m cells — good for live driver tracking
 
/** cell index => list of driver IDs */
$driversByCell = [];
/** driver ID => location */
$drivers = [];
 
function updateDriver(H3 $h3, array &$driversByCell, array &$drivers,
                      string $id, float $lat, float $lng, string $vehicle): void
{
    // Remove from the old cell if the driver moved
    if (isset($drivers[$id])) {
        $old = $drivers[$id]['cell'];
        $driversByCell[$old] = array_filter(
            $driversByCell[$old] ?? [],
            fn ($d) => $d !== $id
        );
    }
 
    $cell = $h3->latLngToCell($lat, $lng, DRIVER_RES);
    $drivers[$id] = ['cell' => $cell, 'lat' => $lat, 'lng' => $lng, 'vehicle' => $vehicle];
    $driversByCell[$cell][] = $id;
}
 
function findNearby(H3 $h3, array $driversByCell, array $drivers,
                    float $lat, float $lng, int $k = 3): array
{
    $riderCell = $h3->latLngToCell($lat, $lng, DRIVER_RES);
    $found = [];
 
    foreach ($h3->gridDisk($riderCell, $k) as $cell) {
        foreach ($driversByCell[$cell] ?? [] as $id) {
            $d = $drivers[$id];
            $found[] = [
                'id' => $id,
                'vehicle' => $d['vehicle'],
                'distance_m' => $h3->greatCircleDistanceM($lat, $lng, $d['lat'], $d['lng']),
            ];
        }
    }
 
    usort($found, fn ($a, $b) => $a['distance_m'] <=> $b['distance_m']);
    return $found;
}
 
// Register a few drivers
updateDriver($h3, $driversByCell, $drivers, 'D001', 23.7925, 90.4078, 'Bike');
updateDriver($h3, $driversByCell, $drivers, 'D002', 23.7937, 90.4066, 'Car');
updateDriver($h3, $driversByCell, $drivers, 'D006', 23.7920, 90.4080, 'Bike');
 
// A rider requests a ride
foreach (array_slice(findNearby($h3, $driversByCell, $drivers, 23.7930, 90.4075), 0, 3) as $i => $d) {
    $eta = max(1, round(($d['distance_m'] / 1000) / 15 * 60));   // ~15 km/h in traffic
    printf("%d. %s (%s) — %.0f m away (~%d min)\n", $i + 1, $d['id'], $d['vehicle'], $d['distance_m'], $eta);
}

Surge pricing reuses the same indexing at a coarser resolution. Count requests and available drivers per zone, then derive a multiplier from the demand-to-supply ratio:

surge.php
const SURGE_RES = 7;   // ~1.4 km zones
 
function surgeMultiplier(H3 $h3, array $demand, array $supply, float $lat, float $lng): float
{
    $zone = $h3->h3ToString($h3->latLngToCell($lat, $lng, SURGE_RES));
    $ratio = ($demand[$zone] ?? 0) / max(1, $supply[$zone] ?? 1);
 
    return match (true) {
        $ratio <= 1 => 1.0,
        $ratio <= 2 => 1.25,
        $ratio <= 3 => 1.5,
        $ratio <= 4 => 1.75,
        default     => min(3.0, 1.0 + $ratio * 0.25),
    };
}

Why two resolutions?

Driver tracking wants small cells (res 9) so "nearby" is genuinely nearby. Surge wants large cells (res 7) so each zone has enough demand and supply to be statistically meaningful. Indexing the same point at two resolutions is a common and cheap pattern.

Food delivery — restaurant search & delivery zones

A delivery app indexes restaurants by cell and searches the same way the ride-sharing app finds drivers. The twist is delivery zones: each restaurant's reach is a disk of cells, stored compacted to save space, and a customer is "in range" if their cell is in that set.

restaurant-search.php
use Foysal50x\H3\H3;
 
$h3 = H3::getInstance();
 
const RES = 9;
 
$byCell = [];
$restaurants = [
    ['id' => 'R001', 'name' => 'Star Kabab',   'cuisine' => 'Bengali', 'lat' => 23.7925, 'lng' => 90.4078],
    ['id' => 'R002', 'name' => 'Pizza Hut',     'cuisine' => 'Italian', 'lat' => 23.7930, 'lng' => 90.4080],
    ['id' => 'R004', 'name' => 'Chillox',       'cuisine' => 'Fast Food','lat' => 23.7920, 'lng' => 90.4085],
];
 
foreach ($restaurants as $r) {
    $cell = $h3->latLngToCell($r['lat'], $r['lng'], RES);
    $byCell[$cell][] = $r;
}
 
function searchNearby(H3 $h3, array $byCell, float $lat, float $lng, int $k = 2, ?string $cuisine = null): array
{
    $cell = $h3->latLngToCell($lat, $lng, RES);
    $hits = [];
 
    foreach ($h3->gridDisk($cell, $k) as $c) {
        foreach ($byCell[$c] ?? [] as $r) {
            if ($cuisine !== null && $r['cuisine'] !== $cuisine) {
                continue;
            }
            $hits[] = $r + ['distance_m' => $h3->greatCircleDistanceM($lat, $lng, $r['lat'], $r['lng'])];
        }
    }
 
    usort($hits, fn ($a, $b) => $a['distance_m'] <=> $b['distance_m']);
    return $hits;
}
 
foreach (searchNearby($h3, $byCell, 23.7928, 90.4079) as $r) {
    printf("- %s (%s) — %.0f m\n", $r['name'], $r['cuisine'], $r['distance_m']);
}

Delivery zones use a disk per restaurant, compacted for storage:

delivery-zones.php
const ZONE_RES = 8;   // ~461 m
 
function setDeliveryZone(H3 $h3, float $lat, float $lng, int $radiusK): array
{
    $center = $h3->latLngToCell($lat, $lng, ZONE_RES);
    $cells  = $h3->gridDisk($center, $radiusK);
 
    return [
        'cells'     => $cells,
        'compacted' => $h3->compactCells($cells),   // 50-90% smaller to store
    ];
}
 
function canDeliver(H3 $h3, array $zone, float $lat, float $lng): bool
{
    $cell = $h3->latLngToCell($lat, $lng, ZONE_RES);
    return in_array($cell, $zone['cells'], true);
}
 
$zone = setDeliveryZone($h3, 23.7925, 90.4078, 4);
printf("Zone: %d cells, %d compacted\n", count($zone['cells']), count($zone['compacted']));
var_dump(canDeliver($h3, $zone, 23.7930, 90.4082));   // true

Store the compacted set, expand on read

Persist the compacted set — it's a fraction of the size. When you need a fast membership test, uncompactCells it back to the zone resolution at load time, or test membership against the compacted set directly by checking each cell's parents.

Fleet management — tracking & geofencing

A logistics fleet tracks vehicles at a fine resolution, records the cells each one visits, and fires alerts when a vehicle enters a geofence. A geofence is just a disk of cells plus its boundary ring.

fleet-tracking.php
use Foysal50x\H3\H3;
 
$h3 = H3::getInstance();
 
const TRACK_RES    = 10;   // ~76 m — precise tracking
const GEOFENCE_RES = 8;    // ~461 m — zones
 
// --- Track a vehicle's path and compute distance travelled ---
$history = [];   // vehicleId => list of points
 
function record(H3 $h3, array &$history, string $vehicle, float $lat, float $lng): void
{
    $history[$vehicle][] = [
        'cell' => $h3->latLngToCell($lat, $lng, TRACK_RES),
        'lat'  => $lat,
        'lng'  => $lng,
    ];
}
 
function distanceKm(H3 $h3, array $points): float
{
    $total = 0.0;
    for ($i = 1; $i < count($points); $i++) {
        $total += $h3->greatCircleDistanceKm(
            $points[$i - 1]['lat'], $points[$i - 1]['lng'],
            $points[$i]['lat'],     $points[$i]['lng']
        );
    }
    return $total;
}
 
$route = [
    [23.7590, 90.3926], [23.7650, 90.3950], [23.7750, 90.4000],
    [23.7850, 90.4050], [23.7925, 90.4078],
];
foreach ($route as [$lat, $lng]) {
    record($h3, $history, 'TRUCK-001', $lat, $lng);
}
 
$pts = $history['TRUCK-001'];
printf("TRUCK-001: %.2f km over %d unique cells\n",
    distanceKm($h3, $pts),
    count(array_unique(array_column($pts, 'cell')))
);

Geofencing tests a vehicle's cell against each fence's cell set, distinguishing inside from on the boundary using a ring:

geofence.php
function makeGeofence(H3 $h3, float $lat, float $lng, int $radiusK): array
{
    $center = $h3->latLngToCell($lat, $lng, GEOFENCE_RES);
    return [
        'center'   => $center,
        'cells'    => $h3->gridDisk($center, $radiusK),
        'boundary' => $h3->gridRing($center, $radiusK),
    ];
}
 
function checkVehicle(H3 $h3, array $fence, float $lat, float $lng): ?array
{
    $cell = $h3->latLngToCell($lat, $lng, GEOFENCE_RES);
 
    if (in_array($cell, $fence['boundary'], true)) {
        return ['status' => 'boundary', 'steps_to_center' => $h3->gridDistance($cell, $fence['center'])];
    }
    if (in_array($cell, $fence['cells'], true)) {
        return ['status' => 'inside', 'steps_to_center' => $h3->gridDistance($cell, $fence['center'])];
    }
    return null;   // outside the fence
}
 
$restricted = makeGeofence($h3, 23.8100, 90.4050, 1);
$alert = checkVehicle($h3, $restricted, 23.8100, 90.4050);
 
if ($alert !== null) {
    printf("ALERT: vehicle %s the restricted zone (%d steps from center)\n",
        $alert['status'], $alert['steps_to_center']);
}

To draw the outline of a service area for a map, keep only the directed edges that face outward — edges whose destination cell isn't part of the area:

outline.php
function outlineEdges(H3 $h3, array $areaCells): array
{
    $set = array_fill_keys($areaCells, true);
    $edges = [];
 
    foreach ($areaCells as $cell) {
        foreach ($h3->originToDirectedEdges($cell) as $edge) {
            if (!$h3->isValidDirectedEdge($edge)) {
                continue;
            }
            $dest = $h3->getDirectedEdgeDestination($edge);
            if (!isset($set[$dest])) {
                $edges[] = $edge;   // a perimeter edge
            }
        }
    }
    return $edges;
}

See Edges & Vertices for turning those edges into drawable geometry with directedEdgeToBoundary.

Where to go next

H3 PHP — Hexagonal geospatial indexing for PHP. Released under the MIT license.