Что далеко ходить – вот даже здесь на Хабре есть выпадающие списки стран, штатов и городов:
Мне (как, наверняка, и вам) много раз приходилось хранить подобные данные (скажем, список стран) и в массивах, и в базах данных, и в файлах конфигурации. Потом нудно добавлять варианты на других языках, когда у проекта появляется альтернативная языковая версия.
Я вижу сразу несколько проблем и буду только рад если кто-то оспорит этот список:
- Добавление географических данных и переводов засоряет рабочий проект, его хранилище данных. Не хочу лишних таблиц, не имеющих отношения к основному бизнесу!
- Географические данные в проекте – это постоянное повторение, изобретение велосипеда заново. Набор стран и городов на планете меняется крайне редко, зачем же тогда отводить этим данным тёплое место в приложении?
- Если в команде нет лингвистов и полиглотов, то в списки городов и стран могут запросто пробраться ошибки. Сколько русских до сих пор пишет «Таиланд» с и кратким?
Географические данные – идеальный кандидат для отдельного пакета. Хочу ввести одну команду в любимом менеджере пакетов (будь то composer, npm или CocoaPods) и сразу получить возможность работать с географическими наименованиями. Хочу иметь что-то вроде модного http://momentjs.com/, но про географию. Поставил один пакет – закрыл эту страницу, так сказать.
Удивительно, но таких пакетов пока не существует, поэтому я начал работу над своим. Для начала – версия на PHP. Эта статья является описанием моего подхода, а основными целями публикации можно считать сбор мнений от других разработчиков; оценку необходимости, актуальности пакета.
Предлагаемая реализация
Вот таким мне представляется оптимальный API для PHP на текущий момент:
// Точка входа в пакет – класс планеты
// В будущем надо добавить возможность создавать объект страны или города без родителя
$planet = new Planet();
// Планета, дай мне все свои страны, и на выходе преобразуй в массив
$planet->getCountries()->toArray();
// Хочу, чтобы имена стран были короткими по возможности – к примеру, США вместо Соединённых Штатов Америки
$planet->getCountries()->useShortNames()->toArray();
// Дайте мне все области Таиланда
$countries = $planet->getCountries();
$thailand = $countries->find(['code' => 'TH']);
$thailand->getStates()->toArray();
// Хочу теперь их на русском
$thailand->getStates()->setLanguage('ru')->toArray();
// Хочу теперь и в другой форме (к примеру, "в Таиланде" вместо "Таиланд")
$thailand->getStates()->setLanguage('ru')->inflict('in')->toArray();
// А где там у нас столица? Есть ли код geonames? А координаты точные?
$capital = $thailand->getCapital();
$capital->getGeonamesId();
$capital->getLatitude();
$capital->getLongitude();
Этого уже достаточно чтобы очень быстро добавить списки из первого изображения на свой сайт.
Для полного счастья хотелось бы ещё:
// Поиск городов конкретной страны по почтовым индексам
$russia->find(['zip' => '626430']);
// Глобальный поиск по почтовым индексам
$planet->find(['zip' => 'EC3R 6DN']);
// Глобальный поиск по координатам
$planet->find([
'latitude' => 51.5078788,
'longitude' => -0.0899208
]);
Мне важно мнение людей на Хабре, но не стану кривить душой – я начал уже писать пакет для PHP не дожидаясь реакции публики. Пилотная версия есть на GitHub, и где-нибудь через месяц в ней появятся 3-4 первых языка и весь базовый функционал. Если кто-то хочет поучаствовать в разработке (особенно интересуют пакеты для других языков – JavaScript, Ruby, et cetera), то буду очень рад получить от вас личное сообщение.
Связь с моделями приложения
Предвижу вопросы вроде «но как же тогда запоминать город пользователя?», и ответ тут достаточно прост – в своей БД (или куда вы там записываете данные) используйте стандартные идентификаторы, такие как коды ISO 3166-1 для стран, коды GeoNames для городов и штатов. Проблем сопоставить код с его содержанием и переводом не будет, и привязаны к этому конкретному пакету вы не станете.
Вопрос
Главный вопрос, он же – цель этой статьи: пользовались ли бы вы таким пакетом в своих приложениях? Или предпочли бы и дальше сами хранить географические данные?
Комментарии (47)
lair
15.05.2016 12:38Даже самые простые сайты, как правило, имеют список стран или городов на какой-нибудь из своих страниц – магазины хотят знать, куда доставлять товары; социальные сети хотят знать, откуда пользователь; и так далее.
Надо заметить, кстати, что в зависимости от задачи формат хранения — разный. Там где соцсеть может себе позволить делать любую нормализацию (потому что эти данные нужны ей и только ей самой), магазин — и вообще любая доставка — должны позволять пользователю ввести адрес так, как тот считает нужным. Да, с подсказками и предложениями исправить предположительно неправильное название, но без иллюзий о том, что магазин лучше знает, как выглядит нужный пользователю адрес.
dusterio
15.05.2016 12:42Ну, выбор страны как правило – все-таки выпадающий список, даже если все остальное в свободном формате вводится. Это по моему опыту онлайн-шоппинга :)
А тут в Австралии так вообще — любят принудительное распознавание адреса. Если сайт не смог распознать (интерпретировать) адрес — пользователь не может продолжить. Таким образом, можно использовать подобный пакет без проблем (точнее даже — он уменьшит количество проблем :)lair
15.05.2016 12:45Ну, выбор страны как правило
Выбор страны можно сделать и по ISO.
А тут в Австралии так вообще — любят принудительное распознавание адреса. Если сайт не смог распознать (интерпретировать) адрес — пользователь не может продолжить.
Сочувствую. Фишка, однако же, в том, что Австралией мир не ограничивается.
dusterio
15.05.2016 12:48Выбор – можно по ISO, а как пользователю отображать? :) А если сайт на 15 языках?
Про Австралию согласен, это так – пример. Про адреса в свободной форме, мне кажется, было бы полезным какой-то умный парсер адресов сделать, который умеет приводить адреса вроде «УЛ НИКИТИНА МОСКВА» в «ул. Никитина, Москва, 125000 Россия». Точно знаю, что некоторым проектам это нужноlair
15.05.2016 12:50Выбор – можно по ISO, а как пользователю отображать?
Речь в этой ветке идет о формате хранения, а не отображения.
Про адреса в свободной форме, мне кажется, было бы полезным какой-то умный парсер адресов сделать, который умеет приводить адреса вроде «УЛ НИКИТИНА МОСКВА» в «ул. Никитина, Москва, 125000 Россия». Точно знаю, что некоторым проектам это нужно
Некоторым, но не всем. Именно поэтому я и говорю, что реальный формат хранения сильно зависит от задачи.
А парсеры такие есть, и на хабре их уже неоднократно упоминали (по крайней мере, в части России), но они, очевидно, языкоспецифичны.
svboobnov
24.05.2016 18:37Речь в этой ветке идет о формате хранения, а не отображения.
Нормализация представления нужна.
В 1С есть такой парсер адресов, и работает примерно так:
* Нормализует введённую строку (по классификатору адресов),
* в базу записывает 3 значения: Код по классификатору, нормализованная строка, строка «как есть» от пользователя (кстати, данные в КЛАДР хранятся в очень понятном формате ключ-значение).
Я эту сохранялку адресов пару раз подгонял под новые стандарты компании, потому знаю, как оно внутри устроено.
Evgeny42
15.05.2016 14:24+2Перемудрили с API, на мой взгляд. И почему Planet а не Earth? Планет то много, а земля одна, родная.
dusterio
15.05.2016 14:31Planet — это класс, а Earth — это конкретный объект, одна из планет :)
Старался сделать все максимально рационально.
Объект Planet всегда один и тот же, и это всегда Земля:
/** * @return string */ public function getShortName() { return 'Earth'; } /** * @return string */ public function getLongName() { return 'The Blue Marble'; }
Evgeny42
15.05.2016 14:42+1Тогда логичнее было бы делать интерфейс планеты, и класс Земли который его имплементирует. Потому что обращаясь к Planet мы не выбираем Землю как планету, то есть это даже не фабрика, а просто Земля, которая названа Planet.
dusterio
15.05.2016 14:58Да, я уже понял ошибку — неудачно выбрано название.
Как минимум, в примере надо сделать имя переменной $earth, чтобы логика соблюдалась :) И объявить, что другие планеты создавать (как перевести instantiate?) пока нельзя
Насчет интерфейсов — я пока склоняюсь (на GitHub код уже видно) к абстрактному классу для всех видов делений вместо интерфейсов для разных уровней делений. Есть класс Divisible (делимый), и пока что логика планеты, стран и штатов почти полностью совпадает — это решение пока похоже на оптимальное.Evgeny42
15.05.2016 15:09Впадать в ООП безумие мне кажется не стоит. Убрать планету вообще, назвать только входной класс Землей, без стандартных методов, типа имени и расположения. Это конечно забавно, но практического смысла не имеет.
А вот что реально было бы хорошо иметь, это разные источники данных, база/файлы/пусть даже интернет. Плюс сделать возможность кеширования тех данных что ты вытаскиваешь.
Поиск по координатам, как я понял устроен топорно и просто сравнивает значения? Это плохо. В некоторых базах есть возможность искать координату приблизительно, да и алгоритмы для поиска есть. Это конечно в пределах пхп работать будет не очень быстро, но хотя бы честно.
Вещи типа useShortNames не совсем понимаю зачем? Если можно возвращать в том же массиве оба варианта. Метод inflict мне тоже не понятен, почему это должна делать библиотека, которая используется как база данных?
В общем, проще засунуть эти данные в монгу, взять какой-нибудь ODM, и будет так же хорошо, и даже намного лучше, ибо сложные запросы, поиск по координатам и т.д.dusterio
15.05.2016 16:47+1А вот что реально было бы хорошо иметь, это разные источники данных, база/файлы/пусть даже интернет. Плюс сделать возможность кеширования тех данных что ты вытаскиваешь.
Про это я уже думаю :)
Поиск по координатам, как я понял устроен топорно и просто сравнивает значения? Это плохо. В некоторых базах есть возможность искать координату приблизительно, да и алгоритмы для поиска есть. Это конечно в пределах пхп работать будет не очень быстро, но хотя бы честно.
Поиска пока нет вообще – он просто показан в примере. В современных БД есть гео-индексы. Как поступить тут и сохранить при этом маленький размер – я ещё думаю.
Вещи типа useShortNames не совсем понимаю зачем? Если можно возвращать в том же массиве оба варианта. Метод inflict мне тоже не понятен, почему это должна делать библиотека, которая используется как база данных?
Сначала так и было – два варианта. Ход мыслей дальше был такой: длинный (полный) вариант есть всегда, короткий иногда. Если возвращать оба поля и одно из них может быть пустым – разработчику придется на своей стороне (приложения) делать условия, а условия уже давно никто не любит.
То есть, если я могу попросить useShortNames(), то после этого я могу просто отображать одно поле, а пакет его выбирает по принципу best effort (старались как могли).
Inflict() – потому что больше половины сайтов до сих пор не умеют правильно склонять слова русского языка. Скажем, у нас есть какая-то лента или стена, и там есть подписи вроде: «Написал Иван 5 минут назад из Владивостока». Где хранить правильную форму имени города?
В общем, проще засунуть эти данные в монгу, взять какой-нибудь ODM, и будет так же хорошо, и даже намного лучше, ибо сложные запросы, поиск по координатам и т.д.
Так все и делают, включая меня – и это занимает много времени, требует установки дополнительных сервисов и так далее. Было бы очень классно иметь легкую, независимую (от внешних демонов) библиотеку, которая покрывает хотя бы все базовые требования.
zelenin
15.05.2016 18:12+1Страны на разных языках есть из коробки в php-intl, хоть и с мудреным апи.
А в Symfony Intl есть удобная абстракция над большинством справочных материалов intl, причем все с использованием iso, а не велосипедия.dusterio
16.05.2016 11:16Спасибо — посмотрел и то, и другое. Но, IMHO, не хватает падежей (хотя это, конечно, только для русского языка актуально) и возможности выбрать менее формальное название. Причем, они это никогда не добавят, потому что в рамках их библиотек это лишнее.
Если сайт на, скажем, английском, и нужен просто список стран — то можно intl использовать.
amaksr
15.05.2016 18:56+1Идея геопакета хороша, но мне кажется основная проблема здесь не как назвать классы и методы, а как максимально автоматично заскриптовать сбор данных и обновления для обновления этого пактета. Понятно, что список стран и крупных городов будет относительно статичным, но все что ниже обновляется очень активно, тем более в масштабах планеты.
Есть много много разных БД, и для разных целей и разных местностей подходят разные базы. Например для России есть регулярно обновляемый КЛАДР, но в нем только адреса, нет геокоординат, нет возможности искать по запросам типа «Кафе у Ашота», и она не переведена на другие языки. У других стран есть свои подобные базы, есть базы OpenStreet, но у всех чего-то не хватает.
В общем, было бы хорошо иметь такую аггрегированную базу всего, и пакет, который бы к с ней работал, но подозреваю, что в итоге получится что-то типа геосервисов Гугла или Яндекса, который одному человеку не потянуть.ainu
15.05.2016 22:27+1Ага. Шаг первый — стандарт базы.
Шаг второй — реализации (данные) поверх стандарта.
Шаг третий — библиотеки поверх данных (например, города России на русском, штаты на английском, города мира на русском, города мира на всех языках, все города на всех языках в конце концов).
bro911
16.05.2016 11:09пользовались ли бы вы таким пакетом в своих приложениях? Или предпочли бы и дальше сами хранить географические данные?
Однозначно, да!
Тривиальная задача, особенно актуальна для разработки сайтов (небольших проектов), т.к. надоедает просто копировать дамп базы из проекта в проект, при этом нужно поддерживать актуальность списка стран, регионов/штатов, городов.
Так же надо учитывать тот факт, что в бд может хранится ненужные геоданные, например, про Африку, что не совсем нужно, когда проект не ориентирован на ее жителей.
RomeroMsk
16.05.2016 15:16Не смотрели в сторону Geocoder? Мне думается, он мог бы помочь в реализации вашего пакета.
dusterio
16.05.2016 15:44Посмотрел только что — мне кажется это больше в сторону трансляции координат в адрес и наоборот, не увидел ничего про локализацию там (а у меня это основная цель).
RomeroMsk
16.05.2016 15:47Локализация поддерживается для тех провайдеров, которые такую возможность предоставляют: http://geocoder-php.org/Geocoder/#locale-aware-providers
m00nk
17.05.2016 19:30Не мешало бы сразу предусмотреть некое деление данных на части. Я о том, что если мой магазин работает только с могилевской областью Беларуси, то мне не хотелось бы в своей БД хранить данные об остальных областях, не говоря уже о всех населенных пунктах, областях, регионах, округах,… более чем двух сотен стран мира.
То же касается и языков. Далеко не каждому сайту нужно более 3-4 языков. Имхо.dusterio
18.05.2016 00:44Да — я сразу это во внимание беру
1) Сами данные будут подгружаться в режиме ленивой загрузи
2) Если базы станут большими, я думаю детализированные базы по странам можно будет выделить в отдельные репозитории. Тогда в начале разработчику просто 2+ пакета надо будет устанавливать — основной и необходимая база, набор языков
nordmine
24.05.2016 18:36Очень интересная идея. Я сам на днях думал о чём-то похожем. Но думал больше с точки зрения того, где брать сами данные? Вот где взять в удобном формате список стран (ну это легко)? А может для какого-то проекта потребуется список всех фильмов, музыкальных исполнителей и т.п. Хотелось бы найти такой ресурс, где всё это доступно в открытом виде и машиночитаемом формате. Есть ли нечто подобное?
lair
А почему именно GeoNames? Это, вроде, не стандарт. Для "штатов" (административного деления) есть тот же ISO 3166-2.
dusterio
Потому что есть ещё города, а для них нет никаких стандартных кодов, насколько я знаю. Штатам, скорее всего, полезно будет всё, что имеется – GeoNames, ISO 3166-2, FIPS. Таким образом будет делом разработчика – к чему привязаться.
Ещё один плюс GeoNames – идентификаторы уникальны для всей планеты. Необязательно хранить код страны рядом с кодом области.
lair
Так чем GeoNames лучше любой другой системы идентификации, хотя бы и собственной?
dusterio
Модный на сегодня ответ – абстракцией. Уменьшением зависимостей, или то что в английском называется 'decoupling'.
Если у меня имеется 5 приложений и всем нужны географические данные, мне проще на всех 5 использовать идентификаторы GeoNames, чем 1) использовать 5 разных наборов 2) придумывать свою конвенцию.
А если выбирать не из своих систем – я не смог ничего глобального найти кроме GeoNames. Если есть что-то еще – то надо будет просто добавить эту систему в пакет, разработчик сможет выбрать, чему он больше доверяет в долгосрочной перспективе :)
lair
Ровно наоборот, вы не уменьшили зависимости, а добавили еще одну — от GeoNames.
dusterio
Если приложений было 5 – вы заменили 5 зависимостей всего одной.
Если приложение было одно – вы заменили собственную кустарную (живущую в адресном пространстве конкретного приложения) зависимость – глобальной, стандартизированной, проверенной годами. Что не так уж и плохо само по себе! ;-)
lair
Нет, потому что
(а) зависимости считаются для каждого приложения отдельно и
(б) собственный код не является зависимостью.
Собственный классификатор не является зависимостью.
GeoNames разве стандарт?
dusterio
Хорошо, согласен про зависимости. Но все еще настаиваю на том, что работать в будущем (то, что называют термином 'maintainability') будет проще с приложением, использующим известные, документированные API и конвенции. В данном случае использовать GeoNames вместо специфичных для конкретного приложения идентификаторов – это как раз переход на API, на конвенцию, которую кто-то уже написал за нас.
Лично мне было бы удобно, что во всех моих приложениях код города 15 – это город X, и при этом мне не надо самому ничего документировать.
GeoNames – это, как говорится, не идеально, но лучшее, что у нас сегодня есть.
lair
Только в том случае, если эта конвенция (а) удобна для приложения (а не надо ее натягивать специально) и (б) известно, что она не изменится.
Кстати, что произойдет в GeoNames, если какой-то город переименуют? Или, что хуже, произойдет административная реорганизация?
dusterio
У них есть поле – альтернативные названия города. Подозреваю, что старое название уйдет туда ;-) ID не сменится
lair
… и что случится в этот момент в вашем интерфейсе в поле "пользователь родился в"?
И это только переименование. А представьте себе, что город удалили (или он разделился на две части). Или представьте себе, что пользователь хочет зачекиниться в городе, которого нет в GeoNames.
dusterio
Вообще, все нормально будет — скажем, до 1991-ого года пользователь имел «родился в Ленинграде», после 91-ого «родился в Санкт-Петербурге» (что абсолютно верно). Ничего не меняя в базе или коде.
Пропадать города будут очень, очень редко. Не уверен, как GeoNames поступают в таких случаях. Если просто удаляют из базы навсегда — то да, надо думать как такие случаи обрабатывать.
В GeoNames есть все города с населением выше 1000 людей — этого достаточно должно быть большинству приложений.
Facebook, к примеру, имея миллиарды капитала, не может до сих пор многие ошибки в географии исправить. Подозреваю, что качество данных было бы выше, если бы любой пользователь мог исправлять ошибки (а в случае пакета на GitHub так и будет)
lair
Это с вашей точки зрения верно, а с его — не обязательно.
Что делать тем, которым недостаточно?
Вы совершенно зря так подозреваете. Даже GeoNames для предоставления более достоверных данных использует ручное курирование.
А главное, что делать, когда интересы пользователей конфликтуют? Например, для одного приложения нужно, чтобы Крым и Севастополь были частью Российской Федерации, для другого — чтобы нет. Как вы будете нормализовать иерархию в этом случае?
dusterio
Ну, пограничные случаи всегда будут.
Если мой пакет будет полезен даже для 50% приложений – это уже будет прекрасный результат.
А использование собственной разработки или доработки, когда общедоступные реализации немного не подходят – это совершенно нормальное явление, его мы наблюдаем ежедневно. Всем не угодить, задача угодить многим :)
Про оспариваемые территории я пока еще думаю над решением :)
lair
Речь не о пакете. Речь о предлагаемом подходе
Особенно неприятно будет, если мы сначала на него обопремся, а потом через год выяснится, что нам нужно использовать значение, отличное от "стандартного идентификатора".
Mendel
Переименован — выводим новое имя.
Расформирован в связи с реорганизацией (удален, разделен, присоединен) — флаг депрекейтед. В зависимости от задачи — или не давать новым указывать, но старых оставлять или давать, но рекомендовать не использовать и т.п.
Неизвестный город? Тут или ручной ввод разрешать или выбирать более общий вариант — область, район, штат, страна…
lair
Это не всегда верно.
Как только вы разрешаете ручной ввод, вы теряете возможность использовать внешние коды как собственные первичные идентификаторы.
Mendel
GUID в рамках всей базы и галочка «не нормализован» для ручного ввода, с последующей заменой на нормализованное значение когда админ до этого доберется. Но именно поэтому я больше сторонник решения с деградацией до страны, чем ручным вводом.
А вообще у каждого своя задача, но некоторый меинстрим существует, и под некритичные задачи что-то универсальное сделать можно. А дальше форкать…
О том, что в магазине нужно давать человеку максимальную свободу я с Вами согласен полностью.
lair
Вот именно поэтому и не надо использовать значения из GeoNames в качестве внутренних идентификаторов.
А так вы теряете пользовательские данные.
Mendel
Ну это от задачи зависит.
Если это форум/социалка и т.п., то указание населенного пункта с точностью до ближайшего пгт (а 1к+ в основном в базе есть) будет вполне себе нормальным вариантом.
Да, согласен с Вами что такая библиотека не будет такой функциональной как момент, и надо будет пилить под задачу, но в качестве дефолтной фишки вполне себе хорошо было бы иметь.
lair
Вот я и написал: в зависимости от задачи формат хранения отличается — но при этом видно, что задач, где можно использовать сторонний идентификатор как основной, не так уж и много.
Хорошо иметь сервис, который умеет отвечать на геозапросы (причем совершенно разных видов), но вот откуда для него брать данные — мне не очень понятно. Предложенная ниже идея аггрегации выглядит наиболее интересной, но это большая задача.