ihangan / laravel-moldova-cuatm
Moldova's CUATM administrative-territorial divisions (raioane, cities, towns and villages) as Eloquent models with multilingual names and coordinates.
Requires
- php: ^8.3
- illuminate/console: ^12.0 || ^13.0
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/database: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- spatie/laravel-package-tools: ^1.92
- spatie/laravel-translatable: ^6.11
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0 || ^11.0
- phpunit/phpunit: ^11.5.50 || ^12.0
This package is auto-updated.
Last update: 2026-06-30 10:48:18 UTC
README
Moldova CUATM for Laravel
Every administrative-territorial unit of the Republic of Moldova, as an Eloquent model you can query straight away. The data comes from CUATM, the official classifier maintained by the National Bureau of Statistics, so the codes and the hierarchy match what government systems use.
You get the 32 raioane, the municipalities, the sectors of Chișinău, every town and all ~1,600 villages, plus Gagauzia and the Stînga Nistrului units. Each one carries its official CUATM code, a parent link, names in Romanian, Russian and Ukrainian, and WGS84 coordinates.
I built this for a rental classifieds site that needed a real location tree (region → city → sector → village) instead of a free-text address field, and pulled it out into a package because the dataset is useful on its own.
Requirements
- PHP 8.3 or higher
- Laravel 12 or 13
Installation
composer require ihangan/laravel-moldova-cuatm
Publish and run the migration, then load the data:
php artisan vendor:publish --tag="moldova-cuatm-migrations"
php artisan migrate
php artisan cuatm:import
cuatm:import is idempotent, so you can run it again whenever a new CUATM
edition ships without ending up with duplicates.
Usage
The model is Ihangan\MoldovaCuatm\Models\Location. It behaves like any other
Eloquent model.
use Ihangan\MoldovaCuatm\Models\Location; use Ihangan\MoldovaCuatm\Enums\LocationType; // By the official CUATM code or by slug. Location::whereCode('0111001')->first(); Location::where('slug', 'chisinau')->first(); // Every raion. Location::ofType(LocationType::Raion)->get(); // Top-level units only. Location::roots()->get();
Names
Names are translatable (backed by spatie/laravel-translatable). Romanian is always present; Russian and Ukrainian exist for most localities; English is filled in for the larger cities.
$chisinau = Location::where('slug', 'chisinau')->first(); $chisinau->name; // current locale $chisinau->getTranslation('name', 'ru'); // "Кишинёв" $chisinau->getTranslation('name', 'uk'); // "Кишинів"
Set a fallback once (for example in a service provider) so a missing locale returns Romanian instead of an empty string:
use Spatie\Translatable\Facades\Translatable; Translatable::fallback(fallbackLocale: 'ro', fallbackAny: true);
Hierarchy
The tree is a self-referencing parent_id.
$village = Location::where('slug', 'dobrogea')->first(); $village->parent; // the town it belongs to $village->ancestors(); // [town, sector, municipality], nearest first $sector = Location::where('slug', 'botanica')->first(); $sector->children; // towns and villages under Botanica
Coordinates
$location = Location::where('slug', 'chisinau')->first(); [$location->lat, $location->lng]; // 47.005..., 28.857...
Facade
A small facade wraps the common lookups when you would rather not write the queries by hand:
use Ihangan\MoldovaCuatm\Facades\Cuatm; Cuatm::findByCode('0111001'); Cuatm::findBySlug('chisinau'); Cuatm::roots(); // raioane, municipalities, Gagauzia, Transnistria Cuatm::raioane(); Cuatm::childrenOf($raion); Cuatm::tree(); // roots with their children eager-loaded
Cascading location picker
roots() and childrenOf() are all you need to build a "pick a region, then a
locality below it" selector. The hierarchy isn't a fixed depth (a raion goes
straight to its villages, while Chișinău goes municipality → sector → town →
village), so the picker keeps offering another dropdown while the chosen unit
still has children.
use Ihangan\MoldovaCuatm\Facades\Cuatm; use Livewire\Component; class LocationPicker extends Component { /** @var array<int, int> the selected location id at each level */ public array $path = []; public function selectLevel(int $level, ?int $id): void { $this->path = array_slice($this->path, 0, $level); // drop the deeper levels if ($id !== null) { $this->path[$level] = $id; } } public function render() { $levels = collect([Cuatm::roots()]); foreach ($this->path as $id) { $children = Cuatm::childrenOf($id); if ($children->isEmpty()) { break; // reached the bottom of the tree } $levels->push($children); } return view('livewire.location-picker', ['levels' => $levels]); } }
{{-- resources/views/livewire/location-picker.blade.php --}} <div class="space-y-3"> @foreach ($levels as $level => $options) <select wire:change="selectLevel({{ $level }}, $event.target.value)"> <option value="">—</option> @foreach ($options as $location) <option value="{{ $location->id }}" @selected(($path[$level] ?? null) === $location->id)> {{ $location->name }} </option> @endforeach </select> @endforeach </div>
The selected location is the last entry in $path. Outside Livewire the same two
calls drive any UI: render Cuatm::roots() first, then Cuatm::childrenOf($id)
each time a level is chosen.
Configuration
Publish the config file if you need to change the table name, the connection or the locales:
php artisan vendor:publish --tag="moldova-cuatm-config"
return [ 'table' => 'cuatm_locations', 'connection' => null, 'locales' => ['ro', 'ru', 'uk', 'en'], 'fallback_locale' => 'ro', ];
The table is named cuatm_locations rather than locations so it does not
clash with one your application may already have.
Data and updates
The dataset lives in database/data/cuatm.json and ships with the package.
- Codes, hierarchy and Romanian names come from CUATM (Clasificatorul unităților administrativ-teritoriale ale Republicii Moldova), published by the National Bureau of Statistics.
- Russian and Ukrainian names are Wikidata exonyms.
- Coordinates are from public geodata.
CUATM changes rarely. When the Bureau publishes a new edition, replace the JSON
file and run php artisan cuatm:import again.
Testing
composer test
Changelog
See CHANGELOG.md and the releases.
Contributing
See CONTRIBUTING.md.
Security
Found a security issue? Email igorhangan@gmail.com instead of using the issue tracker. See SECURITY.md.
Credits
- Igor Hangan
- The administrative data comes from the CUATM classifier published by the National Bureau of Statistics of Moldova, with Russian and Ukrainian names from Wikidata.
License
The MIT License. See LICENSE.md.
The administrative data is public information from the National Bureau of Statistics of Moldova; Wikidata content is CC0.
