beziworld / laravel-uldk
Laravel client for the Polish GUGiK ULDK service: cadastral parcels, buildings, precincts and administrative units by TERYT id or coordinates
Requires
- php: ^8.3
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/phpstan-strict-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-04 16:36:49 UTC
README
Laravel client for ULDK (Usługa Lokalizacji Działek Katastralnych), the free public service of the Polish Head Office of Geodesy and Cartography (GUGiK). Find cadastral parcels, buildings, precincts and administrative units by TERYT identifier or by coordinates, with their geometry:
- parcels by identifier (
141201_1.0001.6509), by precinct name and number (Krzewina 134) or by GPS point - buildings, cadastral precincts (obręby), communes, counties and voivodeships by identifier or point
- geometry as WKT/EWKT or GeoJSON, extents (bounding boxes), WGS 84 or PUWG 1992 output
- snapping to the nearest parcel vertex and merging the area of several parcels
ParcelIdandExistingParcelvalidation rules, anuldk:parcelArtisan command- retries of read-only calls and response caching to stay within the anonymous rate limit
No API key and no contract are needed. Commercial use is free; the data must be attributed to GUGiK (see below).
Installation
composer require beziworld/laravel-uldk
php artisan vendor:publish --tag=uldk-config # optional
Everything works without configuration. Optional .env settings:
ULDK_SRID=4326 # output coordinates: 4326 = WGS 84 (GPS), 2180 = PUWG 1992 ULDK_CACHE_TTL=86400 # seconds; 0 disables caching ULDK_CACHE_STORE= # cache store, empty = default store ULDK_RETRIES=2 # retries after connection errors and HTTP 5xx ULDK_TIMEOUT=30 ULDK_URL=https://uldk.gugik.gov.pl/
Parcels
use Beziworld\Uldk\Data\Point; use Beziworld\Uldk\Facades\Uldk; $parcel = Uldk::parcel('141201_1.0001.6509'); // ?Parcel, null when not found $parcel->number; // "6509" $parcel->region; // precinct (obręb): "Mińsk Mazowiecki" $parcel->commune; // "Mińsk Mazowiecki (miasto)" $parcel->county; // "powiat miński" $parcel->voivodeship; // "mazowieckie" $parcel->extent; // BoundingBox: minX, minY, maxX, maxY, center() $parcel->datasource; // "powiat miński (wfs)" $parcel->communeId(); // "141201_1" $parcel->regionId(); // "141201_1.0001" // Parcel under a GPS point (latitude, longitude), for example a pin dropped on a map $parcel = Uldk::parcelAt(Point::wgs84(52.2297, 21.0122)); // Precinct name and parcel number; names repeat, so several parcels may match $parcels = Uldk::searchParcels('Stara Wieś 756'); // list<Parcel>
Geometry
Pass withGeometry: true to receive the outline. Geometries carry their SRID and convert to GeoJSON for Leaflet,
Mapbox or OpenLayers:
$parcel = Uldk::parcel('141201_1.0001.6509', withGeometry: true); $parcel->geometry->wkt; // "POLYGON((21.5898755 52.1716703, ...))" $parcel->geometry->toEwkt(); // "SRID=4326;POLYGON(...)", ready for PostGIS $parcel->geometry->toGeoJson(); // ['type' => 'Polygon', 'coordinates' => [...]] // PUWG 1992 (EPSG:2180) output for one call chain Uldk::withSrid(2180)->parcel('141201_1.0001.6509', withGeometry: true);
Buildings, precincts and administrative units
use Beziworld\Uldk\Enums\UnitType; Uldk::building('141301_1.0010.713/2.5_BUD'); // ?Building: id, function, parcel, extent... Uldk::buildingAt(Point::puwg1992(677040.0, 481590.0)); Uldk::region('141201_1.0001'); // ?AdministrativeUnit (precinct) Uldk::searchRegions('Krzewina'); // list<AdministrativeUnit>, all precincts of that name Uldk::commune('1412011'); // TERYT with type digit, "141201_1" works too Uldk::county('1412'); Uldk::voivodeship('14'); $commune = Uldk::communeAt(Point::wgs84(50.0614, 19.9449)); // which commune is this point in? $commune->teryt; // "126101_1" $commune->name; // "Kraków (miasto)" $commune->county; // "powiat Kraków" $commune->voivodeship; // "małopolskie" Uldk::unit(UnitType::County, '1412', withGeometry: true); // generic variant
Building coverage depends on the aggregated building database behind ULDK and is much thinner than parcel coverage;
expect null for many points.
Snapping and merged areas
// Nearest parcel boundary vertex within 5 m; the service resolves snapping in PUWG 1992 only $snapped = Uldk::snapToPoint(Point::puwg1992(677047.0, 481559.0), radius: 5); $snapped->point; // Point (EPSG:2180) $snapped->distance; // 0.41 (metres) // One POLYGON for adjoining parcels, a MULTIPOLYGON otherwise (always EPSG:2180) $area = Uldk::aggregateArea(['141201_1.0001.3912', '141201_1.0001.3913']); // Any other operation: data lines of the response, without the status line Uldk::query('GetVoivodeshipById', ['id' => '14', 'result' => 'voivodeship,teryt']); // ['mazowieckie|14']
Validation
use Beziworld\Uldk\Rules\ExistingParcel; use Beziworld\Uldk\Rules\ParcelId; $request->validate([ 'parcel_id' => ['required', new ParcelId()], // format only, no HTTP call 'plot_id' => ['required', new ExistingParcel()], // must exist in the land register 'strict_plot' => ['required', (new ExistingParcel())->strict()], ]);
ExistingParcel passes when ULDK cannot be reached, so an outage does not block your forms; strict() fails the
validation instead.
Artisan
php artisan uldk:parcel 141201_1.0001.6509
php artisan uldk:parcel "Krzewina 134" --geometry
php artisan uldk:parcel --lat=52.2297 --lon=21.0122
Coordinates
| SRID | Name | Axis order in Point and in results |
|---|---|---|
| 4326 | WGS 84 (GPS) | longitude, latitude; build with Point::wgs84($lat, $lon) |
| 2180 | PUWG 1992 (native for Polish cadastre) | easting, northing; build with Point::puwg1992($e, $n) |
Results use ULDK_SRID (default 4326). snapToPoint() and aggregateArea() always answer in EPSG:2180, because
the service ignores the requested SRID for them.
Rate limits, caching and attribution
Anonymous clients may send 200 requests per minute between 6:00 and 18:00, 40 per minute at other hours, and
10,000 per IP address per day. GUGiK can whitelist production IP addresses for free on request
(uslugi.iip@gugik.gov.pl). Responses are cached for ULDK_CACHE_TTL seconds (one day by default) to save the
quota; cadastral data rarely changes.
Wherever ULDK data is displayed, credit the source, for example: "Źródło: Główny Urząd Geodezji i Kartografii (GUGiK), Usługa Lokalizacji Działek Katastralnych (ULDK)."
Errors
| Situation | Result |
|---|---|
Object not found (-1 brak wyników, no vertex within the radius) |
null or an empty list |
| Service error text, for example an invalid parameter | ApiException, serviceMessage() holds the text |
| HTTP error status | ApiException, httpStatus(); isRateLimited() for HTTP 429 |
| Connection failure or timeout (after retries) | ApiException wrapping the ConnectionException |
Empty ULDK_URL or invalid ULDK_SRID |
InvalidConfiguration |
GPS point passed to snapToPoint(), empty id list for aggregateArea() |
InvalidArgumentException |
ApiException and InvalidConfiguration extend Beziworld\Uldk\Exceptions\UldkException.
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues: GitHub issues
Need an integration with a Polish public register or business API? Get in touch through beziworld.eu.
Other packages by the author
laravel-company-data · laravel-polish-validation · laravel-polish-calendar · laravel-nbp
License
MIT. See LICENSE.md. This package is not affiliated with Główny Urząd Geodezji i Kartografii (GUGiK); the data it retrieves remains subject to the GUGiK terms of use.