Logomark

LARAVEL

Un framework qui rend heureux
Voir cette catégorie
Vers le bas
Localisation géographique
Vendredi 21 novembre 2025 17:05

Il arrive fréquemment, lors du développement d'une application, d'avoir à traiter des données géographiques. Il faut par exemple gérer une localisation en termes de région, de département ou de ville, ou alors en termes de coordonnées géographiques. Ces éléments ne sont pas toujours faciles à mettre en œuvre et méritent qu'on s'y attarde quelque peu. Je vous propose donc un petit voyage pour déblayer le terrain. Je me limiterai à la France dans mon exploration, ce qui va déjà bien nous occuper.

Le gouvernement nous offre des API intéressantes concernant le découpage administratif de la France. On a ainsi accès gratuitement aux données concernant les régions, les départements et les communes. Nous allons explorer ces API et voir comment les utiliser judicieusement. Si ces API sont gratuites elles n'en ont pas moins des limitations :

  • 10 requêtes par seconde et par IP pour l’API Découpage administratif ;
  • 2 requêtes simultanées par IP pour le géocodage de masse (maximum 50 Mo par envoi de fichier pour le géocodage direct, 6 Mo pour le géocodage inversé).

Il est donc judicieux d'utiliser ces API côté client. Par contre, côté serveur, on risque de buter sur ces limites selons les traitements et la fréquentation. C'est pour cette raison que je vais aussi montrer comment récupérer toutes les données pour pouvoir effectuer des traitements à partir de votre base.

Les régions

Toutes les régions

L'API concernant les régions est documentée ici. Si vous voulez obtenir toutes les données des régions il suffit d'utiliser cette URL : https://geo.api.gouv.fr/regions. Vous obtenez des données organisées en JSON des 17 régions :

[{"nom":"Île-de-France","code":"11"},{"nom":"Centre-Val de Loire","code":"24"},{"nom":"Bourgogne-Franche-Comté","code":"27"},{"nom":"Normandie","code":"28"},{"nom":"Hauts-de-France","code":"32"},{"nom":"Grand Est","code":"44"},{"nom":"Pays de la Loire","code":"52"},{"nom":"Bretagne","code":"53"},{"nom":"Nouvelle-Aquitaine","code":"75"},{"nom":"Occitanie","code":"76"},{"nom":"Auvergne-Rhône-Alpes","code":"84"},{"nom":"Provence-Alpes-Côte d'Azur","code":"93"},{"nom":"Corse","code":"94"},{"nom":"Guadeloupe","code":"01"},{"nom":"Martinique","code":"02"},{"nom":"Guyane","code":"03"},{"nom":"La Réunion","code":"04"},{"nom":"Mayotte","code":"06"}]

On voit que pour chaque région on a un nom et un code administratif.

Recherche par nom

Un accès bien plus intéressant est celui par nom avec autocomplétion. Avec l'URL : https://geo.api.gouv.fr/regions?nom=p on obtient :

[
  {
    "nom": "Pays de la Loire",
    "code": "52",
    "_score": 0.4168279230811274
  },
  {
    "nom": "Provence-Alpes-Côte d'Azur",
    "code": "93",
    "_score": 0.20428210280544293
  }
]

On peut donc utiliser cette fonctionnalité bien pratique avec un peu de Javascript côté client. Pour l'exemple j'utilise Tailwind, DaisyUI et Alpine :

See the Pen Régions by bestmomo (@bestmomo) on CodePen.

Vous allez voir que l'autocomplétion fonctionne bien :

Récupérer les régions dans la base de données

Parfois vous aurez besoin d'avoir les régions côté serveur dans votre base de données. Comme il n'y en a pas beaucoup, il serait simple de les copier, mais quand même pas très élégant. Je vous propose d'automatiser ça.

En premier, il faut récupérer un fichier JSON avec toutes les données : https://geo.api.gouv.fr/regions. Dans votre navigateur préféré vous devez avoir un bouton pour enregistrer directement le fichier :

Dans votre Laravel enregistrez ce fichier, par exemple ici :

Maintenant, créez un modèle accompagné d'une migration et un seeder :

php artisan make:model Region -ms

Pour la migration on crée deux colonnes :

public function up(): void
{
    Schema::create('regions', function (Blueprint $table) {
        $table->unsignedSmallInteger('code', 5)->unique();
        $table->string('name', 255);
        $table->timestamps();
    });
}

Plutôt que d'utiliser une clé primaire arbitraire autant utiliser le code de la région. Mais il faut informer Eloquent au niveau du modèle :

class Region extends Model
{
    protected $primaryKey = 'code';
    public $incrementing = false;

...

On va avoir un peu de travail dans le seeder pour lire le fichier JSON et remplir la table :

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;

class RegionSeeder extends Seeder
{
    public function run(): void
    {
        $path = database_path('data/regions.json');
        $json = File::get($path);
        $regions = json_decode($json, true);
        $total = count($regions);
        $dataToInsert = [];

        foreach ($regions as $region) {
            $dataToInsert[] = [
                'code' => (integer)$region['code'],
                'name' => $region['nom'],
                'created_at' => now(),
                'updated_at' => now(),
            ];
        }

        DB::table('regions')->insert($dataToInsert);
        $this->command->info("   -> Importé $total régions.");
    }
}

Il ne reste plus qu'à renseigner DatabaseSeeder :

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call(RegionSeeder::class);
    }
}

Et on a ainsi toutes nos régions dans la base sans effort :

Les départements

On va s'intéresser maintenant aux départements. On trouve l'API ici.

Tous les départements

Si vous voulez obtenir toutes les données des départements il suffit d'utiliser cette URL : https://geo.api.gouv.fr/departements. Vous obtenez des données organisées en JSON des 101 départements. Pour chaque département on a ces informations : nom, code et codeRegion. En effet, un département appartient à une région.

Recherche par nom

Un accès bien plus intéressant, comme on l'a vu pour les régions ci-dessus, est celui par nom avec autocomplétion. Avec l'URL : https://geo.api.gouv.fr/departements?nom=t on obtient :

[
  {
    "nom": "Tarn",
    "code": "81",
    "codeRegion": "76",
    "_score": 0.7201337902989691
  },
  {
    "nom": "Tarn-et-Garonne",
    "code": "82",
    "codeRegion": "76",
    "_score": 0.4816336856341112
  },
  {
    "nom": "Territoire de Belfort",
    "code": "90",
    "codeRegion": "27",
    "_score": 0.2628487029678483
  }
]

On peut donc à nouveau utiliser cette fonctionnalité bien pratique avec un peu de Javascript côté client, comme on l'a fait précédemment pour les régions :

See the Pen Département by bestmomo (@bestmomo) on CodePen.

Vous allez voir encore que l'autocomplétion fonctionne bien :

Région ou département

Maintenant pourquoi ne pas proposer un formulaire qui fasse la recherche à la fois pour les régions et les département ?

See the Pen Region ou département by bestmomo (@bestmomo) on CodePen.

Avec toujours l'autocomplétion :

Récupérer les départements dans la base de données

Parfois vous aurez besoin d'avoir les départements côté serveur dans votre base de données. Contrairement aux régions il y en a beaucoup et il vaut mieux automatiser ça.

En premier, il faut récupérer un fichier JSON avec toutes les données : https://geo.api.gouv.fr/departements. Dans votre navigateur préféré vous devez avoir un bouton pour enregistrer directement le fichier :

Dans votre Laravel enregistrez le fichier en compagnie de celui des régions :

Créez un modèle accompagné d'une migration et un seeder :

php artisan make:model Departement -ms

Pour la migration on doit tenir compte de la relation avec les régions :

public function up(): void
{
    Schema::disableForeignKeyConstraints();

    Schema::create('departements', function (Blueprint $table) {
        $table->string('code', 3)->unique();
        $table->string('name', 255);
        $table->timestamps();

        $table->unsignedSmallInteger('region_code');
        $table->foreign('region_code')
            ->references('code')
            ->on('regions')
            ->onDelete('cascade');
    });
}

Pour la clé primaire on ne peut pas utiliser une valeur numérique à cause de la Corse : 2A et 2B.

Il faut informer Eloquent au niveau du modèle :

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Departement extends Model
{
    protected $primaryKey = 'code';
    public $incrementing = false;
    protected $keyType = 'string';

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

J'ai aussi ajouté la relation d'appartenance à la région. Il ne faut pas oublier la réciproque dans le modèle Region :

public function departements(): HasMany
{
    return $this->hasMany(Departement::class);
}

On va avoir encore un peu de travail dans le seeder pour lire le fichier JSON et remplir la table, mais on a pratiquement le même code que pour les régions :

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;

class DepartementSeeder extends Seeder
{
    public function run(): void
    {
        $path = database_path('data/departements.json');
        $json = File::get($path);
        $departements = json_decode($json, true);
        $total = count($departements);
        $dataToInsert = [];

        foreach ($departements as $departement) {
            $dataToInsert[] = [
                'code' => $departement['code'],
                'name' => $departement['nom'],
                'region_code' => (integer)$departement['codeRegion'],
                'created_at' => now(),
                'updated_at' => now(),
            ];
        }

        DB::table('departements')->insert($dataToInsert);
        $this->command->info("   -> Importé $total départements.");
    }
}

Il ne reste plus qu'à renseigner DatabaseSeeder :

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

Normalement, les 101 départements sont dans la table departements.

Les communes

On va s'intéresser maintenant aux communes. On trouve l'API ici. On a un peu plus de choix que pour les départements. 

Toutes les communes

Si vous voulez obtenir toutes les données des communes il suffit d'utiliser cette URL : https://geo.api.gouv.fr/communes. Vous obtenez des données organisées en JSON des 34968 communes. Pour chaque commune on a ces informations : nom, code et codeDepartement. En effet, une commune appartient à un département.

Vous pouvez vous limiter à un département, par exemple https://geo.api.gouv.fr/departements/01/communes. On obtient ainsi les 390 communes de l'Ain.

Les données récupérables ne sont sont pas limitées aux troix attributs évoqués ci-dessus. On peut aussi demander en particulier le ou les codes postaux, la longitude et la latitude, ce qui va nous être très utile par la suite !

Recherche par nom

Comme on l'a vu pour les régions et les départements, on a la recherche par nom avec autocomplétion. Avec l'URL : https://geo.api.gouv.fr/communes?nom=aubai on obtient :

[
  {
    "nom": "Aubais",
    "code": "30019",
    "_score": 0.7278815784178083,
    "departement": {
      "code": "30",
      "nom": "Gard"
    }
  },
  {
    "nom": "Aubaine",
    "code": "21030",
    "_score": 0.7077361062218036,
    "departement": {
      "code": "21",
      "nom": "Côte-d'Or"
    }
  }
]

Recherche par code postal

On peut aussi faire une recherche par code postal. Par exemple avec https://geo.api.gouv.fr/communes?codePostal=34240 on a :

[
  {
    "nom": "Combes",
    "code": "34083",
    "codeDepartement": "34",
    "siren": "213400831",
    "codeEpci": "200042646",
    "codeRegion": "76",
    "codesPostaux": [
      "34240"
    ],
    "population": 321
  },
  {
    "nom": "Lamalou-les-Bains",
    "code": "34126",
    "codeDepartement": "34",
    "siren": "213401268",
    "codeEpci": "200042646",
    "codeRegion": "76",
    "codesPostaux": [
      "34240"
    ],
    "population": 2421
  }
]

Remarquez qu'on peut avoir plusieurs communes pour un code postal, comme dans l'exemple et, à l'inverse, on peut avoir plusieurs codes postaux pour une même commune.

Petite synthèse

On crée un formulaire qui va chercher : région, département, commune par nom et par code postal :

See the Pen Geo by bestmomo (@bestmomo) on CodePen.

On a comme résultat des choses très variées :

Recherche géographique

A partir d'une longitude et d'une latitude on peut trouver quelle commune se situe à cette localisation. Par exemple avec https://geo.api.gouv.fr/communes?lat=43.8&lon=2.11&fields=code,nom,codesPostaux,surface,population,centre on obtient :

[{"code":"81287","nom":"Sieurac","codesPostaux":["81120"],"surface":883.53,"population":263,"centre":{"type":"Point","coordinates":[2.0917,43.8117]}}]

C'est intéressant parce qu'un navigateur, à condition d'obtenir l'autorisation de l'utilisateur, est capable de déterminer les coordonnées géographiques. On peut donc facilement en déduire la commune dans laquelle se trouve cet utilisateur.

Développons un peu cette possiblité avec un exemple complet :

See the Pen Localiser by bestmomo (@bestmomo) on CodePen.

Pour trouver les coordonnées on utilise navigator.geolocation.getCurrentPosition, ça ouvre une formulaire pour obtenir l'autorisation de l'utilisateur. Quand on a cette autorisation, on question l'API pour trouver la commune. Une fois qu'on l'a, on utilise Leaflet pour afficher la carte. C'est une librairie open source gratuite ! Vous trouvez plein de bons tutoriaux ici.

Récupérer les communes dans la base de données

Souvent, vous aurez besoin d'avoir les communes côté serveur dans votre base de données. Contrairement aux régions et départements, il y en a beaucoup, et il vaut mieux automatiser ça.

En premier, il faut récupérer un fichier JSON avec toutes les données intéressantes : https://geo.api.gouv.fr/communes?fields=code,nom,codesPostaux,centre,codeDepartement. Dans votre navigateur vous devez avoir un bouton pour enregistrer directement le fichier, comme on l'a déjà vu pour les régions et les départements.

Dans votre Laravel enregistrez le fichier en compagnie de ceux des régions et départements :

Créez un modèle accompagné d'une migration et un seeder :

php artisan make:model Commune -ms

Pour la migration, on doit tenir compte de la relation avec les départements. Mais il faut aussi tenir compte de la particularité des codes postaux. Comme je l'ai déjà précisé plus haut, une commune peut avoir plusieurs codes postaux et, inversement, un code postal peut correspondre à plusieurs communes. Pour gérer ça correctement en base de données, on doit placer ces codes postaux dans une table distincte et établir une relation de tyle n:n avec les communes. On va donc exclure cette information de code postal de la table des communes.

D'autre part, la gestion de la localisation est un peu particulière. On va utiliser le type mySQL POINT. On va aussi créer un index spécial : SPATIAL INDEX. Il va optimiser toutes les recherches effectuées sur les coordonnées. On aura par exemple une recherche rapide de toutes les communes situées dans un cercle déterminé, comme on va le voir plus loin.

Voici la migration pour la tables communes :

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::disableForeignKeyConstraints();

        Schema::create('communes', function (Blueprint $table) {
            $table->string('code', 5)->unique();
            $table->string('name', 255);
            $table->timestamps();

            $table->string('departement_code', 3);
            $table->foreign('departement_code')
                ->references('code')
                ->on('departements')
                ->onDelete('cascade');
        });

        DB::statement('ALTER TABLE communes ADD coordinates POINT NOT NULL');
        DB::statement('CREATE SPATIAL INDEX idx_coordinates ON communes (coordinates);');
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('communes', function (Blueprint $table) {
            $table->dropIndex('idx_coordinates');
        });
        Schema::dropIfExists('communes');
    }
};

Dans le modèle on doit préciser notre clé primaire particulière :

protected $primaryKey = 'code';
public $incrementing = false;
protected $keyType = 'string';

On va avoir encore un peu de travail dans le seeder pour lire le fichier JSON et remplir la table, mais on a pratiquement le même code que pour les département :

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;

class CommuneSeeder extends Seeder
{
    public function run(): void
    {
        // Chemin vers  fichier JSON
        $path = database_path('data/communes.json');

        // 1. Lecture et décodage du JSON
        $json = File::get($path);
        $communes = json_decode($json, true);
        $chunks = array_chunk($communes, 500); // Insère par lots de 500 pour la performance

        $this->command->info('Début de l\'import des communes...');

        $total = count($communes);
        $inserted = 0;

        foreach ($chunks as $chunk) {
            $dataToInsert = [];

            foreach ($chunk as $commune) {
                $longitude = $commune['centre']['coordinates'][0];
                $latitude = $commune['centre']['coordinates'][1];
                $wkt = "POINT($longitude $latitude)";

                $dataToInsert[] = [
                    'code'             => $commune['code'],
                    'name'             => $commune['nom'],
                    'departement_code' => $commune['codeDepartement'],
                    'created_at'       => now(),
                    'updated_at'       => now(),
                    // Utilisation d'une expression SQL brute pour la colonne POINT
                    'coordinates'      => DB::raw("ST_GeomFromText('$wkt')"),
                ];
            }

            // 2. Insertion en base de données
            DB::table('communes')->insert($dataToInsert);
            $inserted += count($dataToInsert);
            $this->command->info("   -> Importé $inserted / $total communes.");
        }

        $this->command->info('Import des communes terminé !');
    }
}

Pour améliorer la performance on les traite par paquets (chunk) de 500.

Il ne reste plus qu'à renseigner DatabaseSeeder :

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

Après le seeding vous devez avoir toutes les communes dans votre table :

Les codes postaux

On doit aussi créer une table pour les codes postaux. Ils sont tous dans le fichier communes.json, et il suffit d'aller les chercher, mais pour être efficace on doit en même temps établir les relations avec les communes.

Commençons créer un modèle accompagné d'une migration et un seeder :

php artisan make:model Code -ms

La migration va être très simple :

public function up(): void
{
    Schema::create('codes', function (Blueprint $table) {
        $table->string('code', 5)->unique();
        $table->timestamps();
    });
}

Dans le modèle on doit préciser notre clé primaire particulière :

protected $primaryKey = 'code';
public $incrementing = false;
protected $keyType = 'string';

Il nous faut une table pivot entre les communes et les codes postaux :

php artisan make:migration code_commune

Avec cette migration :

public function up(): void
{
    Schema::create('code_commune', function (Blueprint $table) {
        $table->string('commune_code', 5);
        $table->string('code_code', 5);
    });
}

On ajoute les relations.

Dans Commune :

public function codes(): BelongsToMany
{
    return $this->belongsToMany(Code::class);
}

Et dans Code :

public function communes(): BelongsToMany
{
    return $this->belongsToMany(Commune::class);
}

Passons maintenant au seeder. On doit lire le fichier communes.json. On va en extraire les codes postaux et remplir la tables codes et par la même occasion renseigner la table pivot code_commune. Mais ce qui est un peu bête c'est qu'on a déjà utilisé les données pour le seeder des communes. Alors, plutôt que d'ajouter un seeder, autant tout grouper dans le seeder des communes. Il va être un peu chargé, mais ça sera plus efficace. Voilà le nouveau code :

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;

class CommuneSeeder extends Seeder
{
    public function run(): void
    {
        // Chemin vers  fichier JSON
        $path = database_path('data/communes.json');

        if (!File::exists($path)) {
            $this->command->error("Le fichier communes.json n'existe pas dans database/data/.");
            return;
        }

        // 1. Lecture et décodage du JSON
        $json = File::get($path);
        $communesData = json_decode($json, true);

        $this->command->info('Traitement des données...');

        // Tableaux pour stocker les données à insérer
        $codesToInsert = [];
        $pivotsToInsert = [];

        foreach ($communesData as $commune) {
            $communeCode = $commune['code'];

            // Prepare codes postaux et pivot
            if (isset($commune['codesPostaux']) && is_array($commune['codesPostaux'])) {
                foreach ($commune['codesPostaux'] as $cp) {
                    // 1. Prépare Codes
                    if (!isset($codesToInsert[$cp])) {
                        $codesToInsert[$cp] = [
                            'code' => $cp,
                            'created_at' => now(),
                            'updated_at' => now(),
                        ];
                    }

                    // 2. Prépare données du Pivot
                    $pivotsToInsert[] = [
                        'commune_code' => $communeCode,
                        'code_code'    => $cp,
                    ];
                }
            }
        }

        // --- STEP 1: Insertion des codes postaux ---
        $this->command->info('Insertion des codes postaux...');

        // Le découpage en blocs permet d'insérer des milliers de lignes sans atteindre les limites de mémoire.
        // Nous utilisons array_values car DB::insert attend un tableau indexé standard
        $codesChunks = array_chunk(array_values($codesToInsert), 1000);

        foreach ($codesChunks as $chunk) {
            // La fonction insertOrIgnore est utile si vous exécutez le seeder plusieurs fois.
            DB::table('codes')->insertOrIgnore($chunk);
        }

        // --- STEP 2: Insertion des communes ---
        $this->command->info('Insertion des communes...');

        $communeChunks = array_chunk($communesData, 500);
        $total = count($communesData);
        $inserted = 0;

        foreach ($communeChunks as $chunk) {
            $dataToInsert = [];

            foreach ($chunk as $commune) {
                $longitude = $commune['centre']['coordinates'][0];
                $latitude = $commune['centre']['coordinates'][1];
                $wkt = "POINT($longitude $latitude)";

                $dataToInsert[] = [
                    'code'             => $commune['code'],
                    'name'             => $commune['nom'],
                    'departement_code' => $commune['codeDepartement'],
                    'created_at'       => now(),
                    'updated_at'       => now(),
                    'coordinates'      => DB::raw("ST_GeomFromText('$wkt')"),
                ];
            }

            DB::table('communes')->insert($dataToInsert);
            $inserted += count($dataToInsert);
            $this->command->info("   -> Insertion $inserted / $total communes.");
        }

        // --- STEP 3: Insertion dans la table pivot ---
        $this->command->info('Association des communes aux codes postaux...');

        $pivotChunks = array_chunk($pivotsToInsert, 1000);

        foreach ($pivotChunks as $chunk) {
            DB::table('code_commune')->insertOrIgnore($chunk);
        }

        $this->command->info('Importation terminée avec succès !');
    }
}

Quand vous lancez la population maintenant :

  Database\Seeders\RegionSeeder ........................................................... RUNNING
   -> Importé 18 régions.
  Database\Seeders\RegionSeeder ......................................................... 2 ms DONE

  Database\Seeders\DepartementSeeder ...................................................... RUNNING
   -> Importé 101 départements.
  Database\Seeders\DepartementSeeder .................................................... 5 ms DONE

  Database\Seeders\CommuneSeeder .......................................................... RUNNING
Processing data...
Insertion des codes postaux...
Insertion des communes...
   -> Insertion 500 / 34969 communes.
   -> Insertion 1000 / 34969 communes.
	...
   -> Insertion 34969 / 34969 communes.
Association des communes aux codes postaux...
Importation terminée avec succès !
  Database\Seeders\CommuneSeeder .................................................... 2,732 ms DONE

J'ai un peu raccourci pour les communes. Vous devez avoir les tables communes, codes et code_commune correctement renseignées ! Je me retrouve avec 6310 codes postaux, 34969 communes et 35492 enregistrements dans la table pivot.

Recherche par zone

Après avoir vu comment utiliser judicieusement les API et avoir récupéré toutes les données pour les placer dans une base de données locale, voyons ce que nous pouvons faire avec tout cela. Nous avons déjà vu plusieurs exemples d'utilisation des API ci-dessus. Voyons maintenant ce que nous apporte le fait d'avoir les données sur notre serveur. Il est évident qu'il est préférable d'utiliser les API dès lors qu'elles sont suffisantes. J'ai déjà évoqué au début de cet article les limites d'utilisation imposées qui incitent à privilégier l'utilisation de ces API côté client.

Les communes dans une zone

On a déjà vu qu'il est possible d'obtenir la localisation d'un utilisateur s'il l'autorise. On obtient ainsi une longitude et une latitude. Ce qui serait intéressant alors c'est de déterminer dans quelle commune il se situe, et de façon plus générale quelles sont les communes situées dans un certain rayon (parce que l'API permet déjà de déterminer la commune située à certaines coordonnées). On a prévu dans la table communes de mémoriser les coordonnées avec un type POINT. Pour l'optimisation des recherches on a aussi ajouté un index de type SPATIAL. Il est maintenant temps d'utiliser ça. 

On ajoute un scope dans le modèle Commune :

<?php

...
use Illuminate\Database\Eloquent\Attributes\Scope;

class Commune extends Model
{
    ...

    #[Scope]
    protected function distance(Builder $query, float $latitude, float $longitude, float $rayonKm = 10): Builder
    {
        $rayonTerreKm = 6371; // Rayon de la Terre

        return $query->selectRaw("
            communes.*,
            ($rayonTerreKm * ACOS(
                COS(RADIANS(?)) * COS(RADIANS(Y(coordinates))) * COS(RADIANS(X(coordinates)) - RADIANS(?)) +
                SIN(RADIANS(?)) * SIN(RADIANS(Y(coordinates)))
            )) AS distance_km
            ", [$latitude, $longitude, $latitude])
                ->having('distance_km', '<=', $rayonKm)
                ->orderBy('distance_km');
    }

Je ne détaille pas le calcul effectué ! Il devient à présent très facile de trouver les communes dans un certain rayon :

$latitude = 43.4;
$longitude = 2;
$rayonKm = 4;
$communesProches = Commune::distance($latitude, $longitude, $rayonKm)->pluck('name');
Illuminate\Support\Collection {
  #items: array:4 [▼
    0 => "Labécède-Lauragais"
    1 => "Vaudreuille"
    2 => "Issel"
    3 => "Les Brunels"
  ]

Pour les grandes communes on a plusieurs codes postaux, il serait intéressant de les récupérer. Modifions notre code. Déjà le scope :

protected function distance(Builder $query, float $latitude, float $longitude, float $rayonKm = 10): Builder
{
    $rayonTerreKm = 6371; // Rayon de la Terre

    return $query->selectRaw("
        communes.*,
        ($rayonTerreKm * ACOS(
            COS(RADIANS(?)) * COS(RADIANS(Y(coordinates))) * COS(RADIANS(X(coordinates)) - RADIANS(?)) +
            SIN(RADIANS(?)) * SIN(RADIANS(Y(coordinates)))
        )) AS distance_km
        ", [$latitude, $longitude, $latitude])
            ->with('codes')
            ->having('distance_km', '<=', $rayonKm)
            ->orderBy('distance_km');
}

Et le code qui l'utilise :

$latitude = 43.13;
$longitude = 5.93;
$rayonKm = 4;

$communesProches = Commune::distance($latitude, $longitude, $rayonKm)
    ->get()
    ->map(function($commune) {
        return [
            'nom' => $commune->name,
            'codes_postaux' => $commune->codes->pluck('code')
        ];
    });

On trouve la localisation de Toulon et on récupère ses trois codes postaux :

Illuminate\Support\Collection {
  #items: array:1 [▼
    0 => array:2 [▼
      "nom" => "Toulon"
      "codes_postaux" => Illuminate\Support\Collection {#1425 ▼
        #items: array:3 [▼
          0 => "83000"
          1 => "83100"
          2 => "83200"
        ]

Dans le cadre d'une application il est judicieux de faire préciser le code postal.

Conclusion

Mon article est sans doute un peu long mais je pense avoir donné une bonne idée quant à l'utilisation pertinentes des API du découpage administratif français. D'autre part, j'ai montré comment récupérer et mettre en forme les données de ces API pour une utilisation plus spécifique non prévue dans les API, par exemple trouver les communes dans une certaine zone géographique.

Il serait sans doute maintenant intéressant de créer un exemple complet et réaliste comme le formulaire qu'on trouve par exemple sur le site leboncoin. Leurs approche est efficace : ils utilisent si possible la localisation du navigateur, et ensuite la saisie avec complétion avec des données très variées : départements, commune (avec code postal), région... Si vous voulez réaliser le même type de formulaire vous avec pratiquement tous les éléments en main !



Par bestmomo

Aucun commentaire