Geographer library: the first release

If you find yourself implementing something like:
ebay_countries

Or maybe stuff like:
fb_geotag

In your web apps, then meet (drum rolls) Geographer, a library that is now available in the PHP edition. In this article, I will show an example of its integration with one of my own web sites. Actually, that’s how I even got the idea to create this package – I noticed I’m repeating the same stuff again and again in different applications, and as we all know very well – repeating yourself is not something a modern developer should be proud of!

Installation

The package is arleady on Packagist, so you can install it with a single command:


composer require menarasolutions/geographer

There are no extra dependencies and that is one of the goals of the library. I would try to avoid at all costs forcing package users to install extra software or other packages. Nevertheless, I do plan to add some optional integration, eg. Memcached and MongoDB.

Example 1: a simple list of countries

The simplest task of them all, the task we can find at most modern websites. The developer is asked to display a dropdown list of the all the countries in the world, quite often with possibility of switching the output language.

My application did it this way:


public static function getCountryNameByCode($countryCode, $language)
{
    return Config::get('texts.countries')[$language][$countryCode];
}

It’s rather trivial – Config facade gives us access to arrays, and arrays got all the text data. Based on country code key and language key, we can access the right result in the array.

Flaws of this approach:

  • We have to store texts and translations within the application, even though those are presentation layer and are not directly related to our business
  • When we start, all texts and translations have to be added manually. There is no way I could quickly add another language
  • The code is readable, but not very expressive or intuitive

And now let’s try to achieve the same with Geographer:


public static function getCountryNameByCode($countryCode, $language)
{
    return Geographer::findOneByCode($countryCode)
        ->setLanguage($language)
        ->getName();
}

Advantages:

  • Now texts are beyond the application, and from time to time they even update and improve themselves
  • Many popular languages are available right “out of the box”
  • The code is more intuitive, easy to read
  • There is now an opportunity to throw an appropriate exception at a particular stage

Example 2: Inflicted names

This case is more difficult, and this is where Geographer really shines. My site has a page with links like these:
boogie_countries

And SEO-conscious notes like these:
boogie_note

If you have never worked with languages other than English, you may not even be aware that in some languages “in England” and “from England” would be spelled differently, and even prepositions would change depending on circumstances!

The first solution that comes to mind is to add a few extra arrays (dictionaries) to our application – one for each infliction form. As a result, we may end up having hundreds or thousands of translations, and many of them will have to added or edited manually – because most downloadable lists such as Geonames don’t include inflictions.

Anyway, you will end up with something like this:


public static function getCountryNameByCode($countryCode, $language, $form = 'default')
{
    return Config::get('texts.countries')[$language][$countryCode][$form];
}

But sometimes the destination form won’t be provided in the array and we will have to implement some fallbacks programmatically – eg. if there is no correct form for “in”, let’s use the default form and add an ‘in’ preposition. Step by step, our method will grow into a monster with a heap of conditions, or perhaps we will have to add a few extra classes – and all that while our application was about something completely different.

And that’s not all – most of us use view templates and text resource files, and we will have to decide where to keep prepositions – in the city/country array or inside the text (or view) template. Our imaginary template could be “events in :city” or “events :city”. In the first case, languages that have different prepositions depending on the following word won’t be supported properly. In the second, we will end up with a lot of redundancy in dictionaries or additional logic in the code.

Let’s see how it looks with Geographer:


public static function getCountryNameByCode($countryCode, $language, $form = 'default')
{
    return Geographer::findOneByCode($countryCode)
        ->inflict($form)
        ->setLanguage($language)
        ->getName();
}

You can add or remove prepositions any time by calling includePrepositions() and excludePrepositions() methods – this ensures our texts are useful in any text template.
You don’t longer need to consider particularities of a specific language – should the word be inclined? Should I modify the preposition?

Overview of the API

Collection methods

All arrays (countries, states and cities) are implemented as collections and support Fluent API:


$states->sortBy('name'); // Sort by name
$states->setLanguage('ru')->sortBy('name'); // Sort by Russian name
$states->find(['code' => 472039]); // Find all matches
$states->findOne(['code' => 472039]); // Return just the first match
$states->findOneByCode(472,039); // A magic method for your convenience

Common methods

All division classes extend the same parent class and offer common methods:


$object->toArray(); // Return as a regular, flat array
$object->parent(); // Return the parent (a city will return state, a state will return country)
$object->getCode(); // Unique ID
$object->getShortName(); // Colloquial name 
$object->getLongName(); // Official name

All data may be accessed in a number of ways:


$object->getName(); // Via method (name will be inclined if necessary)
$object->name; // As a property
$object['name']; // As an array key
$object->toArray()['name']; // Or get an array first and then a key

You can see full API documentation on GitHub page

Binding your data to geographer divisions

Now we can completely remove all geographical data and corresponding translations from our application because it’s all a part of an external package, but we do still need to somehow bind our models to Geographer units. Let’s say we have a database of users and we want to store a city for each user.

My vision is that developers should be offered as many unique, independent identifiers as possible, and then it’s up to the developer which ID to hook into. In many cases, developers won’t even need to add an extra field because of Geographer (eg. you already had ISO country codes in your database, or Geonames IDs).

At this moment countries have ISO 3611-2, ISO 3611-3 and Geonames codes. States got ISO 3166, FIPS and Geonames codes. Cities only got Geonames codes, and so they are least flexible in terms of binding at this moment.

Therefore, if we are to store user’s city in the database, we can add a geonames_id column to the users table, and then later we will be able to instantiate a Geographer class based on that:


$city = City::build($geonames_id);

Many modern frameworks offer type casting for models so this could even happen automatically. I deliberately chose a variety of international identifiers – developers and their applications should not be tied to Geographer. It shall be as easy to drop it, as it is to start using it!

Current coverage

Dictionaries have all the cities in the world that have population of at least 50 thousand people, and all states of all countries.

Every country has the following data:

  • ISO 3611-2, 3611-3 and Geonames IDs
  • Size (area)
  • National currency
  • Telephone code
  • Population
  • Continent
  • Official language
  • Various forms of country names

Cities and states only have names and identifiers.

Titles have been translated into English, Russian, Spanish, Italian, French, Chinese Mandarin.
Countries are fully covered, states and cities are not but are being constantly updated.

Plans for the future

  • A primitive geospatial index so that user can get the nearest town based on coordinates.
  •     

  • Different languages ​​are likely to be separated into separate GitHub repositories so that developers don’t need to waste bandwidth on unnecessary languages. Moreover, this will be useful when we start implementing other SDKs, eg. Python and Ruby libraries.
  • Our mission is very simple – it’s to become the standard geo-library in the web open source world. We hope that once we reach popularity, we will start getting corrections and fixes from users around the world so the dictionaries become something like a wiki.

    Would love to hear feedback!

    Post by Denis Mysenko

    Born in the snows of Siberia