aldesrahim/laravel-timezone-indonesia

Resolve an Indonesian IANA timezone from a coordinate, offline, from bundled timezone-boundary-builder geometry.

Maintainers

Package info

github.com/aldesrahim/laravel-timezone-indonesia

pkg:composer/aldesrahim/laravel-timezone-indonesia

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-08-13 10:28 UTC

This package is auto-updated.

Last update: 2026-08-13 10:48:51 UTC


README

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.

Read the timings with OPcache in mind. Every millisecond figure below was measured on the CLI with OPcache off, so the compiled numbers include compiling the PHP that OPcache would otherwise have cached. With a warm OPcache the compile half goes away and the absolute times drop by an amount this project has not measured. The memory figures do not move: OPcache shares opcodes, and the array a compiled file returns is built again in every process that requires it.

Retained and peak are different numbers, and the gap is large. Building these arrays costs several times what holding them costs — 1.5 MB of geometry passes through an 11.5 MB peak. Peak is what a memory_limit is measured against and what decides how many workers a box fits; retained is what a long-lived process carries between lookups.

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 — and there the first-lookup cost stops mattering at all, which is worth knowing before optimising for it.

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. Kuala Lumpur sits inside Asia/Jakarta's box and still resolves to nothing.

Optional compile step

php artisan timezone-indonesia:compile

Writes an index to bootstrap/cache/timezone-indonesia.php holding each zone's bounding box, and the geometry itself to bootstrap/cache/timezone-indonesia-zones/, one file per zone. A lookup reads the index and then only the zones whose box contains the point — usually one. Undo it with --clear.

That split is worth more than it sounds, because the four zones are nowhere near equal:

Zone Coordinate pairs Compiled
Asia/Jakarta 481 20 KB
Asia/Jayapura 2,259 90 KB
Asia/Pontianak 14,348 750 KB
Asia/Makassar 19,610 1.1 MB

Java and Sumatra are 1% of the geometry and hold most of the country's people, so what a lookup costs now depends on where the point is. Reading one compiled file, on its own:

File Size Time Retained Peak
index 1 KB 0.2 ms
asia-jakarta.php 25 KB 1.4 ms 0.02 MB 0.11 MB
asia-jayapura.php 120 KB 5.0 ms 0.13 MB 0.85 MB
asia-pontianak.php 774 KB 11.9 ms 0.51 MB 4.07 MB
asia-makassar.php 1058 KB 26.2 ms 0.76 MB 6.16 MB

Before the split every lookup read all four. A first lookup in a fresh process, one lookup per process, single-file compile against per-zone compile:

Point Reads Before After
Jakarta index + 1 zone 39–56 ms, 1.47 MB held, 11.5 MB peak 1.3 ms, 0.08 MB held, 0.12 MB peak
Indian Ocean index only 39 ms, 1.46 MB held, 11.5 MB peak 0.5 ms, 0.05 MB held, 0.06 MB peak
Denpasar index + 2 zones 39 ms, 1.47 MB held, 11.5 MB peak 40 ms, 0.84 MB held, 6.2 MB peak

Denpasar is the honest worst case and worth stating plainly: Bali sits in two boxes, one of them Asia/Makassar, which is the largest file there is. Reading it dominates, so the split saves nothing on time there — only the other zones' memory. The same is true anywhere in Sulawesi, Nusa Tenggara or eastern Kalimantan. What the split reliably buys is Java, Sumatra, Papua, and every coordinate that turns out not to be in Indonesia at all.

A point outside every box answers from the index alone. A point in one box reads that zone. Only the overlaps — Bali, Lombok, the Kalimantan seam — read two. Both halves of the saving survive a warm OPcache in kind if not in size: fewer bytes to compile, and fewer array elements to build.

Files compiled by an earlier version hold every zone inline and carry no format key; they are still read as they were, they simply cannot be loaded a zone at a time. Recompile to get the split.

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.