Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Geonames Laravel Package

michaeldrennen/geonames

Laravel wrapper for the GeoNames geographical database API, offering clean, fluent access to countries, states, cities, postal codes, timezones, and elevation data with built-in caching and simple configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and bootstrap

composer require michaeldrennen/geonames
php artisan vendor:publish --provider="MichaelDrennen\Geonames\GeonamesServiceProvider"
php artisan migrate

Publishing exposes the package config and migrations (the GeoName tables, admin codes, countries, timezones, alternate names, hierarchy, etc.). Read config/geonames.php first — it tells you which connection/tables the package will use and which import flags are on by default.

Load the data

The package ships artisan commands that download GeoNames dumps and import them into your database. A typical first run:

php artisan geonames:install      # all core tables
# or targeted imports:
php artisan geonames:countries
php artisan geonames:admin1
php artisan geonames:admin2
php artisan geonames:features
php artisan geonames:timezones

This is a local database import, not a thin HTTP wrapper. That is the key architectural fact about the package: once imported, your day-to-day queries are Eloquent queries against your own DB, which is orders of magnitude faster and cheaper than hitting geonames.org.

Look at these first

  1. config/geonames.php — connection, table names, import toggles.
  2. The migrations in database/migrations — you need to know geonameid is the primary key and that admin codes are composite (country + code), not globally unique.
  3. The model classes (GeoName, Country, Admin1Code, Admin2Code, Timezone, AlternateName, …) — inspect their relationships before writing joins.

First useful use case: city autocomplete

use MichaelDrennen\Geonames\GeoName;

$term = $request->string('q');

$cities = GeoName::query()
    ->where('name', 'like', $term.'%')
    ->where('feature_class', 'P')          // populated places
    ->orderByDesc('population')
    ->limit(10)
    ->get(['geonameid', 'name', 'country_code', 'admin1_code', 'population']);

return $cities->map(fn ($c) => [
    'id'      => $c->geonameid,
    'label'   => "{$c->name}, {$c->admin1_code}, {$c->country_code}",
    'lat'     => $c->latitude,
    'lng'     => $c->longitude,
]);

That single query replaces an entire third-party geocoding integration for the "pick a city" case.

Implementation Patterns

Treat the import as a read model, not an API proxy. Most applications should query the local tables for search, autocomplete, dropdown lists, timezone resolution, and reverse geocoding. Reserve any live geonames.org API calls for data the dump doesn't cover (elevation, weather, ocean, postal codes), and cache those aggressively.

Hierarchy / breadcrumb resolution. Geography is hierarchical: continent → country → admin1 → admin2 → city. The hierarchy and admin* tables let you walk up instead of storing denormalized strings:

$city = GeoName::find(5128581);

$city->name;           // New York City
$city->country_code;   // US
$city->admin1_code;    // NY
$city->timezone;       // America/New_York (via the timezones table)

Beware that admin1_code alone is ambiguous ("NY" exists in multiple countries). Always scope lookups by country_code as well.

Populate form selects from local tables. Country, admin1, and timezone lists are static enough to cache forever:

$countries = Cache::rememberForever('geonames.countries', fn () =>
    Country::orderBy('name')->get(['iso', 'name'])
);

Reverse geocoding via bounding box + haversine. Don't scan the whole table. Pre-filter with a lat/lng box, then compute distance in PHP or SQL:

$candidates = GeoName::query()
    ->where('feature_class', 'P')
    ->whereBetween('latitude',  [$lat - 0.5, $lat + 0.5])
    ->whereBetween('longitude', [$lng - 0.5, $lng + 0.5])
    ->get();

$nearest = $candidates
    ->sortBy(fn ($c) => haversine($lat, $lng, $c->latitude, $c->longitude))
    ->first();

Schedule the update commands. GeoNames data changes; wire the update command into the scheduler at low frequency (weekly is plenty for most apps):

// routes/console.php or Kernel::schedule()
$schedule->command('geonames:update')->weekly()->withoutOverlapping();

Run it off-peak with onOneServer() if you run multiple app nodes.

Use Eloquent resources / DTOs at the boundary. The raw rows carry dozens of GeoNames columns (feature_class, feature_code, elevation, dem, modification_date, …). Map them to a small resource before they reach your API or front-end; it keeps your UI decoupled from the import schema.

Filter by feature class early. GeoNames is not just cities — it contains mountains, streams, schools, hotels. If you want "places a user can pick", filter feature_class = 'P' and consider feature_code (PPLA, PPLA2, PPLC for capitals/administrative seats) to control result quality.

Integration tip: join, don't N+1. Autocomplete over 10–20 results with country and admin1 accessors will fan out into dozens of queries. Eager-load relations or select the joined columns directly:

GeoName::query()
    ->leftJoin('countries', 'countries.iso', '=', 'geonames.country_code')
    ->where('geonames.name', 'like', $term.'%')
    ->select('geonames.geonameid', 'geonames.name', 'countries.name as country_name')
    ->limit(10)
    ->get();

Gotchas and Tips

The import is heavy. The all-countries dump plus alternate names is multiple gigabytes uncompressed and millions of rows. Expect long runtimes, high disk I/O, and potentially PHP memory limits. Run it from the CLI (never a web request or a deploy hook that can time out), raise max_execution_time/memory_limit, and consider importing alternate names separately only if you actually need multilingual search.

Never run geonames:install in a deploy pipeline. It downloads and re-imports; on a live deploy that's a multi-minute outage and a bandwidth surprise. Use geonames:update (or no-op) in CI/CD and do full installs once, manually or as a one-off provisioning step.

Primary key is geonameid, not an auto-increment. Migrations reflect this. If you build your own tables referencing locations, store the geonameid (unsigned int) and add a foreign key/index — don't assume id.

Admin codes are composite. admin1_code = 'CA' is California in the US and something else elsewhere. Any relation or lookup must include country_code. This is the most common source of "wrong state/province" bugs.

Index your search columns. For LIKE 'term%' you want an index on name (and ideally ascii_name). For LIKE '%term%' an index won't help and a full scan over ~12M rows will hurt — avoid leading wildcards on geonames in request paths, or move to a full-text/Meilisearch/Scout index.

Accents and alternate names. Users type "Munchen", "München", "Munich". The main name column may be the local spelling. Multilingual search requires the alternate_names table (or the denormalized alternate names column); plan for it and add indexes. The alternate names import is the single biggest table — budget for it.

Timezone IDs are not just offsets. Use the IANA identifier (America/New_York) from the timezone table with Carbon, not a stored UTC offset. DST correctness depends on it.

Locale-dependent string comparison. Collation matters for sorting country/city names. Set utf8mb4 and a sensible collation on the connection used by these tables; otherwise accented sorting and equality checks behave differently across environments.

Testing on SQLite can be painful. The package migrations and importers are written with production MySQL/Postgres in mind (indexes, bulk inserts, possibly engine-specific syntax). For tests, either run against the same engine as production or fake the models with fixtures — don't assume :memory: SQLite will run the full import.

The update command may truncate/refresh. Confirm the behavior of the release you pin before running it against tables you've joined to your own data. If you've added foreign keys pointing at geonames.geonameid, a truncate can fail or orphan rows. Snapshot and verify in staging first.

API calls need a GeoNames username and are rate-limited. If you use the live services (rather than the local import), set the username in config/env, honor the free-tier limits (roughly a few thousand credits/day, weighted per endpoint), and cache responses. Don't call geonames.org inside a request loop.

Attribution and licensing. GeoNames data is CC BY 4.0. You must credit GeoNames in your UI/docs. The package's MIT license covers the code, not the data — this is a compliance item, not a nicety.

Pin the version and re-read the README on upgrade. The package has evolved (model set, command names, config keys). A minor-version bump can change relationship names or import flags. Because data import is expensive to re-run, test upgrades with a subset import before committing.

Extension point: subclass the models. If you need custom accessors, scopes, or a different connection, extend GeoName / Country in your app and point your code at your subclasses. That's cleaner than editing published migrations or vendoring the package.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package