laravel-timezone-indonesia maintained by aldesrahim
laravel-timezone-indonesia
Turn a coordinate into an Indonesian IANA timezone, offline.
use Aldesrahim\TimezoneIndonesia\Facades\TimezoneIndonesia;
TimezoneIndonesia::at(-6.2088, 106.8456); // 'Asia/Jakarta'
TimezoneIndonesia::at(1.3521, 103.8198); // null — Singapore, outside Indonesia
The boundaries ship with the package: four polygons filtered from timezone-boundary-builder by aldesrahim/timezone-indonesia. No API, no database, no network call at runtime. Their union is Indonesia, so the same lookup answers both "which timezone" and "is this in Indonesia at all".
Install
composer require aldesrahim/laravel-timezone-indonesia
That is the whole setup. The config file is optional:
php artisan vendor:publish --tag=timezone-indonesia-config
Requires PHP 8.2+ and Laravel 12 or 13.
Usage
| Call | Returns |
|---|---|
at(float $lat, float $lon) |
'Asia/Jakarta', or null outside Indonesia |
zoneAt(float $lat, float $lon) |
Zone enum, or null |
timezoneAt(float $lat, float $lon) |
DateTimeZone, or null |
contains(float $lat, float $lon) |
bool |
dataVersion() |
'2026c' — the upstream release loaded |
metadata() |
provenance: release, timestamp, bounding boxes, checksum |
zones() |
the four Zone cases |
Arguments are (latitude, longitude) everywhere, the order people write coordinates in. GeoJSON
stores [longitude, latitude]; that inversion is handled inside the package. A coordinate that
cannot exist — including latitude and longitude passed the wrong way round — throws
InvalidCoordinate rather than quietly returning null, which would be indistinguishable from
"outside Indonesia".
The Zone enum carries what applications usually want next:
use Aldesrahim\TimezoneIndonesia\Zone;
$zone = TimezoneIndonesia::zoneAt(-5.1477, 119.4327); // Zone::Makassar
$zone->value; // 'Asia/Makassar'
$zone->abbreviation(); // 'WITA'
$zone->offsetHours(); // 8
$zone->region(); // 'South/East/North Kalimantan, Sulawesi, Bali, NTB, NTT'
$zone->timezone(); // DateTimeZone
Zone::tryFromTzid('Asia/Ujung_Pandang'); // Zone::Makassar — legacy aliases resolve
Inject the class if you would rather not use the facade:
public function __construct(private TimezoneIndonesia $timezones) {}
| tzid | Abbreviation | Offset | Covers |
|---|---|---|---|
Asia/Jakarta |
WIB | UTC+7 | Java, Sumatra |
Asia/Pontianak |
WIB | UTC+7 | West and Central Kalimantan |
Asia/Makassar |
WITA | UTC+8 | South/East/North Kalimantan, Sulawesi, Bali, NTB, NTT |
Asia/Jayapura |
WIT | UTC+9 | Maluku, Papua |
What the polygons cover
Internal seas are inside a zone; open ocean is not. Upstream uses territorial waters rather than Exclusive Economic Zones, and in an archipelagic state that puts the water between the islands inside. A coordinate from a pier or a ferry still lands in a named zone, and GPS drift near the coast does not produce a spurious "outside Indonesia".
| Point | Resolves to |
|---|---|
| Java Sea, mid-water | Asia/Jakarta |
| Makassar Strait | Asia/Makassar |
| Banda Sea | Asia/Jayapura |
| Indian Ocean, southwest of Java | null |
| Oecusse, Timor-Leste | null |
| Kota Kinabalu, Kuching, Dili, Singapore, Vanimo | null |
Oecusse is the interesting one: it is a Timor-Leste exclave surrounded by Indonesian West Timor, and
it exists in the data as an interior ring inside Asia/Makassar. Holes are subtracted, so a point
inside it resolves to nothing.
Performance
Measured on PHP 8.5 with the shipped 817 KB file, four polygons, ~36,700 coordinate pairs.
| First lookup in a process | Memory | Per lookup | |
|---|---|---|---|
| Default (GeoJSON) | ~33 ms | 1.4 MB | ~2 ms |
After timezone-indonesia:compile |
~0 with OPcache | 1.4 MB, shared between workers | ~2 ms |
The geometry is read on the first lookup and kept for the life of the process, so an application that installs the package and never calls it pays nothing. Under Octane that means one parse per worker rather than one per request.
Each lookup rejects on bounding boxes first — four float comparisons — and ray casts only the
survivors. The boxes overlap heavily (Asia/Jakarta reaches 116.5°E, past where Pontianak and
Makassar begin), so they can reject but never select.
Optional compile step
php artisan timezone-indonesia:compile
Writes the parsed geometry to bootstrap/cache/timezone-indonesia.php as a plain PHP array, which
OPcache then keeps in shared memory. A deploy step, next to config:cache. Undo it with
--clear. It is worth doing when a single web request does one lookup and 33 ms matters; it is
noise for a queue worker resolving thousands.
Updating the boundaries
The normal path is composer update. A scheduled workflow in this repository checks upstream
monthly, verifies the checksum, runs the suite against the new data, and opens a pull request;
merging it publishes a minor release.
If you need boundaries before the package ships them:
php artisan timezone-indonesia:update # download the latest release
php artisan timezone-indonesia:update --check # exit 1 when newer boundaries exist
php artisan timezone-indonesia:update --tag=2026c
The download is verified against the SHA-256 in its own metadata and parsed before anything is
written, so a truncated or foreign file cannot replace a working one. Files land in
storage/app/timezone-indonesia (configurable) and take precedence over the bundled copy; delete
them to go back. If a compiled file exists it is rebuilt automatically. This command is the only
network code in the package, it never runs on its own, and everything works with the network
disabled.
Testing
composer test
The point fixtures are ported verbatim from the data repository's validation script: 15 coordinates
that must resolve to a specific zone, 10 that must resolve to nothing. Kota Kinabalu and Christmas
Island are load-bearing — both pass on the comprehensive upstream build and both fail on the merged
-1970 variant — so they are what stops a future data sync from quietly shipping Malaysian
territory as Indonesian.
Licensing
The code is MIT (LICENSE). Everything under resources/data/ is Open Database License
1.0 (DATA-LICENSE), because it derives from OpenStreetMap.
Timezone boundaries from timezone-boundary-builder, derived from OpenStreetMap data, © OpenStreetMap contributors, ODbL 1.0.
Keep that attribution with any copy you distribute. Returning a timezone for a coordinate in an API
response is a Produced Work and needs attribution only; redistributing the boundary file itself
keeps it under ODbL. The line also lives inside the GeoJSON's attribution property, so it survives
being copied out of this repository.