bordeux/geoname-bundle
Symfony bundle to import and access GeoNames.org geographic data in PostgreSQL via Doctrine ORM. Load countries, timezones, states/provinces, and cities/towns/suburbs from the GeoNames export using built-in console import commands.
Installation:
composer require bordeux/geoname-bundle
Note: If using Laravel, install Symfony console components first:
composer require symfony/console symfony/dependency-injection
Configure Database:
Ensure your config/database.php uses PostgreSQL (required for v3.0+). Example:
'connections' => [
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public', // Critical for bundle's schema
'sslmode' => 'prefer',
],
],
Run Schema Update: For Laravel, create a custom Artisan command to mimic Symfony’s schema update:
php artisan make:command GeoNameSchemaUpdate
Then update the command:
// app/Console/Commands/GeoNameSchemaUpdate.php
public function handle()
{
$this->call('migrate', ['--path' => 'vendor/bordeux/geoname-bundle/src/Resources/migrations']);
}
Run it:
php artisan geoname:schema-update
Import GeoNames Data: Create a Laravel Artisan command to trigger the import:
php artisan make:command GeoNameImport
Update the command to call the bundle’s importer:
// app/Console/Commands/GeoNameImport.php
public function handle()
{
$importer = new \Bordeux\Bundle\GeoNameBundle\Command\GeoNameImportCommand();
$importer->setContainer($this->app); // Laravel's service container
$importer->run(new \Symfony\Component\Console\Input\ArrayInput([]), new \Symfony\Component\Console\Output\ConsoleOutput());
}
Execute:
php artisan geoname:import
Warning: This downloads ~350MB of data. Run in a non-production environment first.
First Query: Use Eloquent to query the imported data. Example for cities:
// app/Models/GeoNameCity.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class GeoNameCity extends Model
{
protected $table = 'geoname_cities';
protected $primaryKey = 'geonameid';
public $incrementing = false;
protected $keyType = 'integer';
}
Query:
$cities = GeoNameCity::where('name', 'like', '%Paris%')->get();
Data Normalization:
// Find a city by alternate name
$geoname = \App\Models\GeoNameCity::where('alternatenames', 'like', '%NY%')->first();
if ($geoname) {
$standardizedName = $geoname->name . ', ' . $geoname->country->name;
}
Illuminate\Support\Facades\Cache:
$geoname = Cache::remember("geoname_{$alternateName}", now()->addHours(1), function () use ($alternateName) {
return \App\Models\GeoNameCity::where('alternatenames', 'like', "%{$alternateName}%")->first();
});
Geospatial Enrichment:
// Enrich a user with their city's timezone
$user->city = \App\Models\GeoNameCity::where('geonameid', $user->city_id)->first();
$user->timezone = $user->city->timezone->name;
with() to eager-load relationships:
$user = User::with(['city.timezone'])->find($userId);
Validation:
public function validateLocation($locationName)
{
return \App\Models\GeoNameCity::where('name', $locationName)->exists();
}
// app/Rules/ValidGeoName.php
use Illuminate\Contracts\Validation\Rule;
use App\Models\GeoNameCity;
class ValidGeoName implements Rule
{
public function passes($attribute, $value)
{
return GeoNameCity::where('name', $value)->orWhere('alternatenames', 'like', "%{$value}%")->exists();
}
}
Usage:
$request->validate([
'city' => ['required', new ValidGeoName],
]);
Console Automation:
app/Console/Kernel.php:
protected function schedule(Schedule $schedule)
{
$schedule->command('geoname:import')->monthly()->onOneServer();
}
php artisan geoname:import --entity=countries
Doctrine ORM in Laravel:
doctrine/dbal and use it alongside Eloquent:
use Doctrine\DBAL\Connection;
$conn = new Connection(['url' => 'postgres://user:pass@localhost/db']);
$stmt = $conn->executeQuery('SELECT * FROM geoname_cities WHERE name = ?', ['Paris']);
DB facade with raw SQL:
$cities = DB::select('SELECT * FROM geoname_cities WHERE name = ?', ['Paris']);
PostGIS Integration:
spatie/laravel-postgis for geospatial queries:
composer require spatie/laravel-postgis
use Spatie\Postgis\PostgisFacade as Postgis;
$nearbyCities = Postgis::select('geoname_cities')
->where('ST_DWithin', 'ST_GeomFromText(:point, 4326)', ['point' => 'POINT(-73.935242 40.730610)', 'radius' => 10000])
->get();
Alternate Names:
// Split alternatenames for full-text search
DB::statement("
CREATE INDEX idx_geoname_cities_alternatenames ON geoname_cities
USING gin (to_tsvector('english', alternatenames))
");
DB:
$results = DB::select("
SELECT * FROM geoname_cities
WHERE to_tsvector('english', alternatenames) @@ to_tsquery('english', ?)
", ['NY | New York']);
Caching Strategies:
// app/Providers/AppServiceProvider.php
public function boot()
{
Cache::rememberForever('geoname_cities', function () {
return \App\Models\GeoNameCity::all()->keyBy('geonameid');
});
}
remember for query results:
$cities = Cache::remember("cities_{$countryId}", now()->addHours(6), function () use ($countryId) {
return \App\Models\GeoNameCity::where('country_code', $countryId)->get();
});
Event-Driven Updates:
// app/Listeners/UpdateGeoNamesData.php
public function handle()
{
Artisan::call('geoname:import', ['--entity' => 'cities
How can I help you explore Laravel packages today?