Search by

beziworld / laravel-uldk

kamcw

Laravel client for the Polish GUGiK ULDK service: cadastral parcels, buildings, precincts and administrative units by TERYT id or coordinates

Package info

github.com/Beziworld/laravel-uldk

Homepage

Issues

pkg:composer/beziworld/laravel-uldk

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-11-04 15:49 UTC

This package is auto-updated.

Last update: 2026-11-04 16:36:49 UTC


README

tests Latest Version License

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
  • ParcelId and ExistingParcel validation rules, an uldk:parcel Artisan 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).

Polska wersja dokumentacji

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.

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.