Search by

siberfx / turkiye-address

siberfx

Laravel package for seeding Turkish address (city, district, neighborhood) tables from SQL dumps.

Package info

github.com/siberfx/turkiye-il-ilce-mahalle

pkg:composer/siberfx/turkiye-address

Statistics

Installs: 1 795

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 0

2.3.0 2026-10-30 18:00 UTC

This package is auto-updated.

Last update: 2026-10-30 18:02:12 UTC


README

Tests Latest Version on Packagist Total Downloads PHP Version Laravel GitHub Release License Data

A Laravel package that ships the complete administrative address hierarchy of Türkiye — 81 cities (il), 973 districts (ilçe) and 94,933 settlements (33k mahalle, 18k köy, 43k bağlı such as mevkii and mezra) — together with migrations, a fast seeder and Eloquent models with relationships.

  • Works out of the box: migrations auto-load and the seeder reads the bundled SQL dumps, so nothing has to be published.
  • Database-agnostic seeder, tested in CI on MySQL, PostgreSQL and SQLite: chunked upserts inside a single transaction, with foreign key checks left on.
  • Mahalle, köy and bağlı told apart by a type column, with every bağlı (mevkii, mezra, …) linked to its köy.
  • Synced with the official Ministry of Interior inventory: every il, ilçe, mahalle, köy and bağlı is present.
  • Configurable table names, foreign key column names and model classes.
  • Laravel 12 and 13, PHP 8.3+.

Table of contents

Requirements

Package version Laravel PHP
2.x 12, 13 8.3 – 8.5

Installation

composer require siberfx/turkiye-address

The service provider (Siberfx\TurkiyePackage\TurkiyeAddressServiceProvider) is registered automatically through package discovery.

Quick start

# Create the cities, districts and neighborhoods tables and import the data
php artisan turkiye:migrate --seed

Or use the standard Laravel workflow — the package migrations are loaded automatically:

php artisan migrate
php artisan db:seed --class="Siberfx\TurkiyePackage\Database\Seeders\TurkiyeSeeder"

To seed together with the rest of your application, call it from database/seeders/DatabaseSeeder.php:

use Siberfx\TurkiyePackage\Database\Seeders\TurkiyeSeeder;

public function run(): void
{
    $this->call(TurkiyeSeeder::class);
}

The seeder upserts by ID and never truncates or deletes. Re-running it — for example after upgrading the package — adds new records and refreshes existing ones in place, so rows in your own tables that reference cities, districts or neighborhoods are left untouched, and records you deactivated (is_active = false) stay deactivated. Foreign key checks stay enabled, so it also works on managed databases without superuser rights.

Database schema

cities                  districts                 neighborhoods
──────────              ──────────────            ──────────────────
id          PK          id          PK            id           PK
name                    city_id     FK → cities   city_id      FK → cities
slug        unique      name                      district_id  FK → districts
sort_order              slug                      name
is_active               is_active                 slug
                                                  type         mahalle | koy | bagli
                                                  parent_id    FK → neighborhoods (köy of a bağlı)
                                                  is_active
  • cities.id is the official plate code (1 = Adana, 6 = Ankara, 34 = İstanbul, …).
  • cities.sort_order puts the configured big cities first and the rest in Turkish alphabetical order. See City order.
  • neighborhoods.city_id is denormalised so you can filter neighborhoods by city without a join.
  • neighborhoods.type tells mahalle, köy and bağlı apart; parent_id links a bağlı (mevkii, mezra, …) to its köy. See Mahalle, köy and bağlı.
  • Foreign keys cascade on delete. Tables have no timestamps.
  • Names are stored in upper case as published officially (e.g. İSTANBUL, KADIKÖY); slugs are ASCII (istanbul, kadikoy).

Usage

Querying

use Siberfx\TurkiyePackage\Models\City;
use Siberfx\TurkiyePackage\Models\District;
use Siberfx\TurkiyePackage\Models\Neighborhood;

// All cities, alphabetically
$cities = City::orderBy('name')->get();

// A city by plate code or slug
$istanbul = City::find(34);
$izmir    = City::where('slug', 'izmir')->first();

// Districts of a city
$districts = District::where('city_id', 6)->orderBy('name')->get();

// Neighborhoods of a district
$neighborhoods = Neighborhood::where('district_id', 1103)->get();

// Search neighborhoods by name inside a city
$results = Neighborhood::where('city_id', 34)
    ->where('name', 'like', '%BAĞDAT%')
    ->limit(20)
    ->get();

// Key/value list for a <select>
$options = City::orderBy('name')->pluck('name', 'id');

Relationships

Model Relationship Type
City districts() HasMany
City neighborhoods() HasMany
District city() BelongsTo
District neighborhoods() HasMany
City villages() HasMany
District villages() HasMany
Neighborhood district() BelongsTo
Neighborhood city() BelongsTo
Neighborhood parent() BelongsTo
Neighborhood children() HasMany
$city = City::find(35);

$city->districts;                 // all districts of İzmir
$city->neighborhoods()->count();  // number of neighborhoods in İzmir

$district = District::with('city')->find(1103);
echo "{$district->name} / {$district->city->name}";   // ADALAR / İSTANBUL

// Full address line for a neighborhood
$neighborhood = Neighborhood::with(['district', 'city'])->find(1);
echo implode(', ', [
    $neighborhood->name,
    $neighborhood->district->name,
    $neighborhood->city->name,
]); // AHMET REMZİ YÜREĞİR MAHALLESİ, SEYHAN, ADANA

// Eager-load counts
$cities = City::withCount(['districts', 'neighborhoods'])->get();

// Cities with more than 20 districts
$big = City::has('districts', '>', 20)->get();

Mahalle, köy and bağlı

The neighborhoods table holds the three kinds of settlement below a district, told apart by type (cast to the NeighborhoodType enum):

type Enum What it is Example name
mahalle NeighborhoodType::Mahalle Neighborhood, including mahalles inside a belde (town) KADIKÖY MAHALLESİ, KÖMÜR BELDESİ, GAZİ MAHALLESİ
koy NeighborhoodType::Koy Village AKDURAK KÖYÜ
bagli NeighborhoodType::Bagli Bağlı: a mevkii, mezra, yayla evleri, … that belongs to a köy (parent_id) AKDURAK KÖYÜ, ÇAKMAK MEZRASI

Mahalle and köy are the same administrative level (both sit directly under the district and have a muhtar), so they share the table; a bağlı points at its köy through parent_id.

use Siberfx\TurkiyePackage\Enums\NeighborhoodType;
use Siberfx\TurkiyePackage\Models\District;
use Siberfx\TurkiyePackage\Models\Neighborhood;

$district = District::find(1182); // BESNİ

// Only mahalles / only köys / only bağlı
$mahalleler = $district->neighborhoods()->mahalle()->orderBy('name')->get();
$koyler     = $district->villages()->orderBy('name')->get();     // same as ->neighborhoods()->koy()
$bagli      = $district->neighborhoods()->bagli()->get();
$sameThing  = Neighborhood::ofType(NeighborhoodType::Koy)->where('district_id', 1182)->get();

// The bağlı of a köy, and the köy of a bağlı
$koy = Neighborhood::find(4308);                   // AKDURAK KÖYÜ
$koy->children->pluck('name');                     // AKDURAK KÖYÜ, ÇAKMAK MEZRASI, …
$koy->children()->first()->parent->is($koy);       // true

// Branch on the type
if ($neighborhood->type === NeighborhoodType::Bagli) {
    $label = $neighborhood->parent?->name;
}

// Counts per type for a city
Neighborhood::where('city_id', 2)
    ->selectRaw('type, count(*) as total')
    ->groupBy('type')
    ->pluck('total', 'type');      // ['bagli' => …, 'koy' => …, 'mahalle' => …]

A typical address form offers mahalle and köy, and the bağlı only once a köy is chosen:

Route::get('/api/districts/{district}/settlements', fn (District $district) =>
    $district->neighborhoods()->whereIn('type', ['mahalle', 'koy'])->active()->orderBy('name')->get(['id', 'name', 'type'])
);

Route::get('/api/villages/{koy}/bagli', fn (Neighborhood $koy) =>
    $koy->children()->active()->orderBy('name')->get(['id', 'name'])
);

City order (big cities first)

Most users live in a handful of provinces, so city pickers usually list the big cities first instead of starting at Adana. City::ordered() does that:

City::ordered()->pluck('name');
// İSTANBUL, ANKARA, İZMİR, ADANA, ADIYAMAN, AFYONKARAHİSAR, AĞRI, … ZONGULDAK

City::active()->ordered()->get(['id', 'name']);   // for a <select>
  • The cities listed first come from featured_cities in the config (plate codes). The default is the three most populous provinces: İstanbul (34), Ankara (6), İzmir (35).
  • All other cities follow in Turkish alphabetical order (ÇANAKKALE after BURSA, IĞDIR before ISPARTA before İZMİR, ŞANLIURFA after SİVAS), regardless of the database collation.
  • The order is stored in cities.sort_order (1, 2, 3, …), so you can also orderBy('sort_order') in your own queries and joins.

Change the featured cities in .env or the config, then recalculate:

TURKIYE_FEATURED_CITIES=34,6,35,16,7    # İstanbul, Ankara, İzmir, Bursa, Antalya
php artisan turkiye:sort-cities           # from the config
php artisan turkiye:sort-cities 34 35 16  # or pass plate codes directly

The seeder and the migration that adds the column apply the configured order automatically.

Active scope

Every row has an is_active flag (cast to bool). Deactivate records you don't want to offer and filter with the active() scope:

District::where('city_id', 34)->update(['is_active' => false]);
District::whereIn('id', [1103, 1166])->update(['is_active' => true]);

$offered = District::active()->where('city_id', 34)->get();

$city->districts()->active()->get();

Validation

use Illuminate\Validation\Rule;

$request->validate([
    'city_id' => ['required', 'integer', 'exists:cities,id'],

    'district_id' => [
        'required', 'integer',
        Rule::exists('districts', 'id')->where('city_id', $request->integer('city_id')),
    ],

    'neighborhood_id' => [
        'required', 'integer',
        Rule::exists('neighborhoods', 'id')->where('district_id', $request->integer('district_id')),
    ],
]);

The scoped Rule::exists checks make sure the district belongs to the selected city and the neighborhood to the selected district.

Dependent dropdowns (city → district → neighborhood)

Routes / controller:

use Illuminate\Support\Facades\Route;
use Siberfx\TurkiyePackage\Models\City;
use Siberfx\TurkiyePackage\Models\District;

Route::get('/api/cities', fn () =>
    City::active()->orderBy('name')->get(['id', 'name'])
);

Route::get('/api/cities/{city}/districts', fn (City $city) =>
    $city->districts()->active()->orderBy('name')->get(['id', 'name'])
);

Route::get('/api/districts/{district}/neighborhoods', fn (District $district) =>
    $district->neighborhoods()->active()->orderBy('name')->get(['id', 'name'])
);

Front end (vanilla JS):

<select id="city"></select>
<select id="district" disabled></select>
<select id="neighborhood" disabled></select>

<script>
const fill = async (select, url) => {
    const items = await (await fetch(url)).json();
    select.innerHTML = '<option value="">Select…</option>' +
        items.map(i => `<option value="${i.id}">${i.name}</option>`).join('');
    select.disabled = false;
};

const city = document.getElementById('city');
const district = document.getElementById('district');
const neighborhood = document.getElementById('neighborhood');

fill(city, '/api/cities');
city.addEventListener('change', () => {
    neighborhood.disabled = true;
    fill(district, `/api/cities/${city.value}/districts`);
});
district.addEventListener('change', () =>
    fill(neighborhood, `/api/districts/${district.value}/neighborhoods`)
);
</script>

Livewire 3 component:

use Livewire\Component;
use Siberfx\TurkiyePackage\Models\City;
use Siberfx\TurkiyePackage\Models\District;
use Siberfx\TurkiyePackage\Models\Neighborhood;

class AddressPicker extends Component
{
    public ?int $cityId = null;
    public ?int $districtId = null;
    public ?int $neighborhoodId = null;

    public function updatedCityId(): void
    {
        $this->reset('districtId', 'neighborhoodId');
    }

    public function updatedDistrictId(): void
    {
        $this->reset('neighborhoodId');
    }

    public function render()
    {
        return view('livewire.address-picker', [
            'cities' => City::orderBy('name')->pluck('name', 'id'),
            'districts' => $this->cityId
                ? District::where('city_id', $this->cityId)->orderBy('name')->pluck('name', 'id')
                : collect(),
            'neighborhoods' => $this->districtId
                ? Neighborhood::where('district_id', $this->districtId)->orderBy('name')->pluck('name', 'id')
                : collect(),
        ]);
    }
}

Attaching addresses to your own models

// Migration
Schema::create('addresses', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->foreignId('city_id')->constrained('cities');
    $table->foreignId('district_id')->constrained('districts');
    $table->foreignId('neighborhood_id')->nullable()->constrained('neighborhoods');
    $table->string('line');
    $table->string('postal_code', 5)->nullable();
    $table->timestamps();
});
// app/Models/Address.php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Siberfx\TurkiyePackage\Models\City;
use Siberfx\TurkiyePackage\Models\District;
use Siberfx\TurkiyePackage\Models\Neighborhood;

class Address extends Model
{
    protected $fillable = ['city_id', 'district_id', 'neighborhood_id', 'line', 'postal_code'];

    public function city(): BelongsTo
    {
        return $this->belongsTo(City::class);
    }

    public function district(): BelongsTo
    {
        return $this->belongsTo(District::class);
    }

    public function neighborhood(): BelongsTo
    {
        return $this->belongsTo(Neighborhood::class);
    }

    public function fullAddress(): string
    {
        return collect([
            $this->line,
            $this->neighborhood?->name,
            $this->district->name . ' / ' . $this->city->name,
            $this->postal_code,
        ])->filter()->implode(', ');
    }
}

Caching

The address data rarely changes, so it is a good candidate for caching:

use Illuminate\Support\Facades\Cache;

$cities = Cache::rememberForever('turkiye.cities', fn () =>
    City::orderBy('name')->get(['id', 'name', 'slug'])
);

$districts = Cache::rememberForever("turkiye.cities.{$cityId}.districts", fn () =>
    District::where('city_id', $cityId)->orderBy('name')->get(['id', 'name'])
);

Clear these keys after re-seeding or toggling is_active.

Artisan commands

turkiye:migrate

Runs only the package migrations.

php artisan turkiye:migrate            # run the package migrations
php artisan turkiye:migrate --seed     # ...and import the data
php artisan turkiye:migrate --force    # allow running in production
php artisan turkiye:migrate --fresh    # ⚠ drops ALL tables of the database first (migrate:fresh)

turkiye:sort-cities

Recalculates cities.sort_order: featured cities first, then Turkish alphabetical order. See City order.

php artisan turkiye:sort-cities            # featured cities from config('turkiye-package.featured_cities')
php artisan turkiye:sort-cities 34 6 35 16 # İstanbul, Ankara, İzmir, Bursa first

turkiye:publish

Publishes package files into your application. With no option, everything is published.

php artisan turkiye:publish                # config + migrations + seeder & SQL dumps
php artisan turkiye:publish --config       # config/turkiye-package.php
php artisan turkiye:publish --migrations   # database/migrations/*
php artisan turkiye:publish --seeders      # database/seeders/TurkiyeSeeder.php + database/sql-dumps/*.sql
php artisan turkiye:publish --force        # overwrite existing files

The same files can be published with vendor:publish:

php artisan vendor:publish --provider="Siberfx\TurkiyePackage\TurkiyeAddressServiceProvider" --tag=config

Available tags: config, migrations, seeders.

Configuration

Publish the config file if you need to change the defaults:

php artisan turkiye:publish --config
// config/turkiye-package.php
return [
    // Table names
    'cities_table' => env('TURKIYE_CITIES_TABLE', 'cities'),
    'districts_table' => env('TURKIYE_DISTRICTS_TABLE', 'districts'),
    'neighborhoods_table' => env('TURKIYE_NEIGHBORHOODS_TABLE', 'neighborhoods'),

    // Foreign key column names
    'cities_relation_id' => env('TURKIYE_CITIES_RELATION_ID', 'city_id'),
    'districts_relation_id' => env('TURKIYE_DISTRICTS_RELATION_ID', 'district_id'),

    // Cities listed first by City::ordered(); default İstanbul, Ankara, İzmir
    'featured_cities' => array_map(intval(...), array_filter(explode(',', (string) env('TURKIYE_FEATURED_CITIES', '34,6,35')))),

    // Model classes used by the relationships
    'city_model' => Siberfx\TurkiyePackage\Models\City::class,
    'district_model' => Siberfx\TurkiyePackage\Models\District::class,
    'neighborhood_model' => Siberfx\TurkiyePackage\Models\Neighborhood::class,
];

Example — avoid clashing with an existing cities table by prefixing all tables via .env:

TURKIYE_CITIES_TABLE=tr_cities
TURKIYE_DISTRICTS_TABLE=tr_districts
TURKIYE_NEIGHBORHOODS_TABLE=tr_neighborhoods

Migrations, seeder and models all read these values. Set them before running the migrations.

Customising

Your own models

Extend the package models to add attributes, scopes or relationships, then point the config at them so the package relationships return your classes:

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Siberfx\TurkiyePackage\Models\City as BaseCity;

class City extends BaseCity
{
    // "İSTANBUL" -> "İstanbul"
    protected function displayName(): Attribute
    {
        return Attribute::get(fn () => mb_convert_case(
            mb_strtolower($this->name, 'UTF-8'), MB_CASE_TITLE, 'UTF-8'
        ));
    }
}
// config/turkiye-package.php
'city_model' => App\Models\City::class,

Tip: mb_strtolower does not apply Turkish casing rules for I/İ. If exact Turkish casing matters, use Str::lower() after replacing I → ı and İ → i.

Your own seeder or data

php artisan turkiye:publish --seeders creates:

  • database/seeders/TurkiyeSeeder.php — a class extending the package seeder, where you can override any step (importCities(), importDistricts(), importNeighborhoods(), dumpPath(), …).
  • database/sql-dumps/*.sql — editable copies of the data. When these files exist the seeder uses them instead of the bundled dumps.
namespace Database\Seeders;

use Illuminate\Support\Facades\DB;
use Siberfx\TurkiyePackage\Database\Seeders\TurkiyeSeeder as BaseTurkiyeSeeder;

class TurkiyeSeeder extends BaseTurkiyeSeeder
{
    public function run(): void
    {
        parent::run();

        // Only offer neighborhoods proper (skip köy / mevkii / mezra entries)
        DB::table($this->neighborhoodsTable)
            ->where('name', 'not like', '%MAHALLESİ')
            ->update(['is_active' => false]);
    }
}
php artisan db:seed --class="Database\Seeders\TurkiyeSeeder"

About the data

Level Rows Notes
Cities 81 IDs are the official plate codes
Districts 973 Every province's district count matches the official administrative list
Neighborhoods 94,933 33,097 mahalle · 18,377 köy · 43,459 bağlı (mevkii, mezra, yayla evleri, …)

The neighborhood level started from the postal (PTT) address register and has been completed from the official Ministry of Interior lists (see below). Every köy has its own row, and 42,997 bağlı are linked to their köy through parent_id; the remaining 462 are legacy postal entries whose köy no longer exists officially (most became mahalles in the 2012 metropolitan reform), so their parent_id is null. Street (sokak) level data is not included.

Source and official reference

İl, ilçe, mahalle, köy and bağlı records are checked against the official inventory of administrative divisions published by the Ministry of Interior (İçişleri Bakanlığı, İller İdaresi Genel Müdürlüğü):

Türkiye Mülki İdare Bölümleri Envanteri — e-icisleri.gov.tr

Official list (October 2026) Official count Present in this package
İl (province) 81 all
İlçe (district) 922 + 51 MERKEZ all
Mahalle (neighborhood) 32,327 all
Köy (village) 18,210 all
Bağlı (attached to a köy) 23,570 all 23,567 places¹

¹ The official bağlı list also contains 3 water administrations attached to a metropolitan municipality; they are not places and are not imported.

  • Records that were missing from the original dataset were added with IDs starting at 1,000,000, so existing IDs — and any foreign keys pointing at them in your application — never change.
  • Names use the official spelling. About 2,000 records that differed by a single letter (e.g. KEKLİKÇİ → KEKLİKÇE) were renamed in place, keeping their IDs; when a köy is renamed, its bağlı are renamed with it. Note that renamed records also get a new slug.
  • Records are never deleted or renumbered by a data update. When a place was renamed or moved to another district, the current record has its own row and the old row is kept with is_active = false (28 rows, listed with the reason in src/Database/sql-dumps/inactive.json). Filter with active() to hide them.

Found a missing or outdated record? Please open an issue with the official source.

Testing

composer install
composer test

Locally the suite runs against an in-memory SQLite database. CI runs it on PHP 8.3, 8.4 and 8.5 with Laravel 12 and 13, and additionally against MySQL 8.4 and PostgreSQL 17. To run it against your own database, set the DB_* variables:

DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=testing DB_USERNAME=postgres DB_PASSWORD=secret vendor/bin/phpunit
Test file Covers
tests/Feature/SeederTest.php Full import: idempotent re-runs, exact row counts, counts per type, referential integrity, bağlı → köy links, records synced from the official lists, escaped names, published-dump override
tests/Feature/NeighborhoodModelTest.php type enum cast and default, mahalle() / koy() / bagli() / ofType() scopes, parent() / children(), villages(), ON DELETE SET NULL for bağlı
tests/Feature/ReseedTest.php Re-seeding keeps application rows that reference the address tables and is_active flags; parents written before children with foreign keys on; ID sequences usable afterwards
tests/Feature/CustomConfigurationTest.php Custom table and foreign key column names across migrations, seeder and models
tests/Feature/ServiceProviderTest.php Provider registration, turkiye:migrate, turkiye:publish, model relationships, active() scope
tests/Feature/CityOrderingTest.php City::ordered(), default featured cities, turkiye:sort-cities, TURKIYE_FEATURED_CITIES, ordering an existing installation on migrate
tests/Unit/CityOrderTest.php Turkish alphabetical comparison, featured-first ordering
tests/Unit/NeighborhoodTypeTest.php Deriving mahalle / köy / bağlı from a name, stable enum values

Run a single file or test:

vendor/bin/phpunit tests/Feature/NeighborhoodModelTest.php
vendor/bin/phpunit --filter test_seeder_sets_types_and_links_bagli_to_their_koy

Changelog

See CHANGELOG.md for what has changed between versions.

Contributing

Contributions are welcome! Please open an issue first to discuss the change you would like to make.

License

This package is open-sourced software licensed under the MIT license.

Contact

SiberFX — info@siberfx.com

Project: https://github.com/siberfx/turkiye-il-ilce-mahalle