Разбор строки адреса (улица [дом]) средствами Golang и Postgis

    Hi, %habrauser%.
    Столкнулся я на днях с интересной задачей — пользователь вводит строку, которая может быть улицей с домом, просто улицей или вообще не улицей, а нам надо узнать имел ли он ввиду улицу с домом и соответствующее ему подсказать.
    — Казалось бы чего проще — разбей строку по пробелу и наслаждайся — подумал Штирлиц
    — А как насчет улицы Павла Корчагина — шепнула птица Обломинго
    — Эм, ну номер дома же наверняка число — сказал Штирлиц
    — Ага, корп1 — хорошее число
    — Мдя, придется изобретать велосипед


    И расчехлил Штириц плюсомет Golang, да зарядил в него Postgis...


    И так, что мы имеем — устройство случайного ввода пользователя, некую строку и острую необходимость выполнить определенное действие в зависимости от того, является ли то, что ввел пользователь улицей с домом

    const MARK_STEP = 20
    func AnalyzeString(str string) (result int, street, house string) {
    	result = 100
    	LastSpace := strings.LastIndex(str, " ")
    	if LastSpace < 1 {
    		result = 0
    		street = str
    		return result, street, house
    	}
    	if LastSpace < (len([]rune(str)) - 6) {
    		result -= MARK_STEP
    	} else {
    		result += MARK_STEP
    	}
    	street = str[:LastSpace]
    	house = str[LastSpace+1:]
    	if models.StreetCount(street) > 0 {
    		result += MARK_STEP * 2 
    	} else {
    		result -= MARK_STEP * 2 
    	}
    	if models.StreetCount(str) > 0 {
    		result -= MARK_STEP
    	} else {
    		result += MARK_STEP
    	}
    	if models.HouseCount(street, house) > 0 {
    		result += MARK_STEP 
    	} else {
    		result -= MARK_STEP * 4 
    	}
    	var int_count, char_count uint8
    	for _, run := range []rune(house) {
    		if (run > 47) && (run < 58) {
    			int_count++
    		} else {
    			char_count++
    		}
    	}
    	switch {
    	case char_count == 0:
    		{
    			result += MARK_STEP * 3
    		}
    	case int_count == 0:
    		{
    			result -= MARK_STEP * 4
    		}
    	case int_count == char_count:
    		{
    			result += MARK_STEP
    		}
    	case int_count > char_count:
    		{
    			result += MARK_STEP * 2
    		}
    	case char_count > int_count:
    		{
    			result -= MARK_STEP
    		}
    	}
    	return result, street, house
    }
    


    И так, что же это за функция и что она делает?
    Функция принимает на вход строку введенную пользователем и анализирует ее возвращая вероятность того, что это улица с домом, отдельно улицу и отдельно дом.
    Если вероятность больше 200 — можете не сомневаться — пользователь имел ввиду улицу с домом.

    Вероятно вы заметили вызовы StreetCount(street) и HouseCount(street, house)
    в принципе за ними кроется два банальный SQL запроса

    rows, err := DB.Query("SELECT COUNT(*) FROM planet_osm_line WHERE highway <> '' AND name ILIKE $1 ", "%"+name+"%")
    

    и
    rows, err := DB.Query("SELECT COUNT(house.*) FROM planet_osm_polygon AS house WHERE \"addr:street\" ILIKE $1 AND \"addr:housenumber\" ILIKE $2", "%"+streetName+"%", "%"+houseNum+"%")
    

    соответственно

    И так, теперь по порядку
    Стартовая вероятность 100, разбиваем строку по последнему пробелу, если не получилось (нет пробела) — то к чертям все это не улица с домом.
    Если же получилось, то смотрим сколько символов осталось после пробела, если меньше 6 (это число, как и многие другие подобраны по методу профессора Тыка Неба Пальца), то стоит увеличить вероятность, а если больше или равно, то уменьшить.

    Уже как-то изменив вероятность, или вообще выйдя из функции плакаться к гарбаж коллектору, продолжаем нашу эпопею (ну или не продолжаем, дядя Гарбажа не любит отпускать).
    Все, что до последнего пробела считаем улицей, а то, что после — домом.
    Дальше просим Postgis посмотреть, сколько улиц похоже на то, что ввел наш пользователь, если таковые вообще есть, то повышаем вероятность, если нет, то уменьшаем (нет, не выходим, ведь вполне возможно, что улицы еще нет в базе).
    Теперь попробуем поискать в базе улицы по исходной строке, если есть, то стоит придержать коней и понизить вероятность, а если нет, то приударим галопом.
    Повторим ту-же самую операцию с домами, спроси Postgis о том, есть ли дома с похожим номером, стоящих на похожих улицах.

    Ну все, больше база нам не понадобиться.
    Теперь разложим строку на символы и посчитаем сколько у нас есть цифр, а сколько букв и других символов, думаю соответствующую конструкцию switch — case объяснять не надо.

    Вот таким вот не хитрым макаром Штирлиц выполнил очередное задание ставки, поборол птицу Обломинго, спас дракона от принцессы, подменяя марио
    Всем спасибо, все свободны, а я, Штирлиц, пойду почитаю дальше хабру.
    Поделиться публикацией

    Похожие публикации

    Комментарии 14
      +2
      Вообще-то по моему опыту задача определения, является ли строка адресом в реальности гораздо сложнее, чем кажется. В свое время когда я работал над Yahoo! Geoplanet, мы решали эту задачу, и при ее решении есть великое количество подводных камней. Если есть возможность, нужно использовать существующее решение:


      В конечном счете (при реализации) это упирается в построение достаточного умного классификатора, который выдает вероятность того, что строка или адрес, или обозначает какое-то географическое место.
        0
        Определение является ли строка адресом — согласен, более сложная задача, в моем же случае строка 100% является адресом, и надо определить, что вводит пользователь, название места, город, улицу или улицу с домом, при чем определить по базе (да, я знаю, что у меня есть вот это:
        нет, не выходим, ведь вполне возможно, что улицы еще нет в базе

        но это просто «тычка» на будущее)
        Мне не тягаться с Yahoo, мне только адрес подсказать.
        0
        А Ваша система знает об адресах без улиц? А то вот у нас тут г.Москва, г.Зеленоград, корп.1610, например — это нормальный полный адрес (дважды город, нет улицы и вместо номера дома — корпус). 95% систем это не может обработать.
          0
          С городами и корпусом справится, в вот без улицы тяжко, хотя, у нас пользователь редко город вводит, в основном он тянется из координат, спасибо за мысль об отсутствии улицы, на днях буду дорабатывать
            0
            Всегда пожалуйста, а то грустно нам тут в Зеленограде, вот Спутник запустили — и опять про Зеленоград забыли, адреса на находятся. И так почти всегда — вроде Москва а вроде и не Москва — даже в заточенных под одну Москву сервисах адрес часто не проходит.
              0
              Ну, я Вас тоже расстрою, не думаю, что сервис, под который писалась система, маленькую часть которой я тут представил, скоро доберется до России
                0
                Ну, есть призрачная надежда, что кто-то прочтёт Ваш пост, мой комментарий и в его российской системе будет на одни грабли меньше :)
                  0
                  И на одни костыли больше :P
            +1
            Попробуйте dadata, без проблем спарсит любой адрес Зеленограда.
            +2
            Применительно к OSM, не забывайте об addr:place — туда вместо addr:street пишутся адреса не привязанные к линейным объектам (улицам), т.е., например, деревни и районы. Таких адресов в России довольно много. Так-же, по-хорошему, должны обозначаться адреса в Зеленограде.

            если не получилось (нет пробела) — то к чертям все это не улица с домом

            «Ленина,11»

            if LastSpace < (len([]rune(str)) — 6) {

            «11стр123»

            case char_count > int_count: { result -= MARK_STEP }

            «2корп2»

            ILIKE $1 ", "%"+name+"%"

            Это будет очень плохо работать — в OSM названия улиц приведены к одному полному виду («улица Ленина»), пользователь же может ввести статусную часть с другой стороны или с сокращением (Ленина улица, ул.Ленина) и в addr:street это не найдётся. Также всё сломается если пользователь допустит пробел в номере дома («11 корп 2» — «11 корп» будет отнесено к улице и также не найдётся ILIKE'ом).

            \«addr:housenumber\» ILIKE $2"

            А это, в случае когда в номере дома есть корпус/строение — ещё хуже. Пользователь может ввести «1Ас1», «1АС1», «1А стр1», «1А стр. 1», в OSM же будет только что-то одно (принято 1Ас1, но за этим никто особо не следит, так что возможны варианты).

            ИМХО, алгоритм состоит из слабо обоснованной эвристики и в целом имеет низкую эффективность. Я, к сожалению, с разбором адресов пока не сталкивался, но руководствовался бы следующими соображениями:

            1) Номер дома состоит из ограниченного набора токенов (\d+[: русская буква:]?, /, с\.?, к\.?, стр\.?, корп\.? и ещё немного редких), и, скорее всего, выделяется не слишком монструозной регуляркой (особенно если мы ожидаем его в фиксированной позиции — в конце строки).
            2) Улицу можно выделить по словарю — у вас так и делается, но не учитывается всё богатство вариаций.

            Я меня есть проект который как раз используется для приведения названий улиц в OSM к единому стилю (Ленина ул -> улица Ленина) (про который я обещал написать пост, но руки никак не дойдут). Его работа основана на приведении названий улиц к виду, инвариантному относительно встречающихся изменений в написании (сюда входит различное написание статусной части как-то «переулок», «пер.», «пер-к», регистр и порядок слов), таким образом и «пер-к. Ульянова» и «УЛЬЯНОВА ПЕРЕУЛОК» и «пер ульянова» будут приведены к одной и той же строке («переулок ульянова») по которой можно понять что это одно название и найти для него в словаре полное и правильное написание, причём за O(1) (а также определить что в названии отсутствует статусная часть и даже найти название с заданным количеством опечаток используя нечёткий поиск).

            Проект тут, если интересно.

            Но для вашего случая можно всё сильно упростить: просто взять все названия из OSM, разбить по словам, привести к нижнему регистру, добавить того чего нет в OSM но может ввести пользователь (сокращения статусных частей, «им.», «имени»), запихнуть в словарь (даже база для этого не нужна, он будет занимать единицы мегабайт), и искать в нём каждое слово из запроса.

            Итого, выкусываем регуляркой как можно более длинный номер дома справа, то что осталось разбиваем по словам и ищем каждое в словаре — при достаточном совпадении считаем улицей и всё вместе — адресом. Если хочется дополнительно проверить его на существование в OSM, то и его и OSM нужно будет приводить к одному виду как описано выше. Номер дома, как было сказано, тоже.
              +1
              >Стартовая вероятность 100, разбиваем строку по последнему пробелу, если не получилось (нет пробела) — то к чертям все это не улица с домом.
              Как быть с «Ленина,3» или «Кирова-7-21»
              с последним пробелом все тоже несколько сложнее, например, «Ульянова 27/12 корп 3»
              Сами улицы пользователи могут писать с большим разнообразием: проспект Сахарова, улица Сахарова, улица им. академика Сахарова, улица имени Сахарова, пр-т ак. Сахарова — будет ли это работать при сравнении с эталонной базой улиц?

              >Мне не тягаться с Yahoo, мне только адрес подсказать.
              Если только адрес подсказать, почему не хотите воспользоваться одним из существующих решений по автокомплиту адресов, например, dadata.ru/suggestions/?
                0
                Комментарии как всегда интересней статьи. :) К слову — наверное по этой причине до сих пор в OSM нет нормального поисковика по адресам, чего к слову очень не хватает.
                  0
                  Кстати да, в OSM я тоже не могу найти Зеленоградский адрес приходящими мне в голову способами (находит только сам город при том, что город отлично отрисован в плане объектов), Гугл-карты в этом плане чуть лучше — хоть что-то пытаются угадать, правда мимо, и только Яндекс справляется на отлично. Вот ещё предложенный выше сервис dadata вняв моим мольбам тоже умеет обрабатывать Зеленоград :)
                  +1
                  Лет 10 назад я такие задачи решал путем поиска похожих хешей, например «олгогра енински прсп 1 234», затем из менее десятка пхжх адресов уже спокойно распарсивал под кладр.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое