
Границы вкуса
Карта мира по названиям блюд на вывесках: где кончается плов и начинается бирьяни — и кто на самом деле проводит эти границы.
Открыть проект ↗Вопрос
Кто на самом деле проводит «границы вкуса» — горы и реки или язык?
В OpenStreetMap отмечено больше двух миллионов заведений общепита почти по всему миру. Смотрю у каждого его название, и если в нем есть название блюда — крашу землю вокруг него в цвет этого блюда. Континенты раскрашиваются сами: здесь сплошной «плов», через пару сотен километров — «бирьяни», дальше — «пилав». Границы между цветами и есть линии, которые нигде не проведены, но всем примерно понятны.
Что выяснилось
- 01
В горах Гиндукуша заканчивается плов и начинается бирьяни.
- 02
В основном границы вкуса рисует культура, а не рельеф (но иногда они совпадают).
- 03
Дёнер, шаверма, гирос — одно слово «крутящийся» на трёх языках.
- 04
kebap— в Германии дёнер, в Турции мясо на шампуре: одно слово, два блюда.
Зачем
В Threads попался пост:

Строго говоря, жареный рис, плов и паэлья — блюда разные, хоть и родственные. Но наблюдение верное: у похожих блюд есть свои области, и на стыке одно название сменяется другим. Где именно проходит этот стык, никто не измерял. А мне захотелось это измерить и нанести на карту.
Что-то вроде разрушителей мифов, только для дата-мемов: берёшь красивое утверждение из интернета и проверяешь его на реальных данных.
Как это сделано
Вывеска заведения сообщает ровно одно: каким словом называют блюдо в этой точке. Почему здесь плов, а в паре сотен километров бирьяни — по ней не понять (хотя строить догадки можно). Поэтому карта отвечает на вопрос «ГДЕ?» и ничего не говорит про «ПОЧЕМУ?».
Всё, что ниже, я достал из самих данных. Они распределены неравномерно: в некоторых местах OpenStreetMap заметно менее популярен. Поэтому надёжнее всего выводы, которые сохраняются даже с учётом этого перекоса.
Главный вывод, переживший все проверки: почти каждая граница вкуса повторяет границу ЯЗЫКА (или культуры, если хотите), а не рельефа. Самый наглядный пример — славянская стена пельменей: по дороге на восток pierogi уступают место вареникам, а те — пельменям, и смена идёт по языковой границе.

Это то, что лингвисты называют изоглоссой, и тут проект оказывается в хорошей компании. Ещё в 1880-х Георг Венкер разослал по школам Германской империи 40 фраз и впервые нарисовал диалектные границы. Обнаружил странное: границы легли не вдоль Рейна и не по горам, а веером разошлись по забытым старым политическим рубежам.
И приятно: две из самых виральных карт в истории интернета были именно про названия еды — американская «soda vs pop vs coke» и карта сэндвичей Джошуа Каца (2013). Но обе охватывают одну страну и основаны на опросах. «Границы вкуса» берут ту же формулу и делают её глобальной, опираясь на реальные вывески.
Один шов выделяется: плов и бирьяни расходятся по Гиндукушу — пожалуй, единственное место, где вкусовая граница легла рядом с горами. Но и здесь имя блюду даёт не хребет, а люди по обе его стороны. Горы препятствовали культурному обмену, так что смешения плова и бирьяни не произошло.

В Китае жареный рис — просто обед: на входе будет название ресторана, а не 炒饭. Из 234 тысяч китайских заведений эти иероглифы стоят лишь на 129 (просто 129, не тысяч). «Экспортный» слой показывает обратное: в самой Греции находится лишь 9% всех гиросных мира, а в США — 38%; 51% заведений с дёнером — в Германии; почти 90% заведений с хинкали находятся в российских городах, и только 4% — в Грузии.
Дома блюдо — просто обед; на чужбине его имя на вывеске становится флагом.
На постсоветском пространстве находятся 97% всех заведений с вывеской «Шашлык»; в Японии — 84% заведений с якитори, в Индонезии — 85% с сатаем, в Турции — 86% с кюфтой. И моя любимая находка: дёнер, шаверма и гирос — это одно и то же слово, «крутящийся». Турецкое dönmek «вертеться», арабская шаверма (через çevirme) «поворот», греческое γύρος «оборот» — один османский вертел, три языка независимо назвали его «тем, что крутится».
Ещё из той же серии: пельмени — не славянское слово, а финно-угорское «ухо-хлеб» (коми/удмуртское пельнянь). Паэлья — это «сковорода» (латинское patella): в меме «плов → паэлья» слова друг другу чужие, чистая рифма. Сувлаки восходит к латинскому «шилу». Одно персидское polow разъехалось в плов, пилав, пулао, пилаф, пелау.
Идея общая, а слово каждый язык чеканит сам.
Заведений больше у одних блюд, а территории — у других: пустая клетка достаётся блюду, которое преобладает в ближайшем скоплении точек. Забавный случай — лапша: раменных в мире больше всех (почти 9700), а картой владеет паста (54% суши).
Первая версия стояла на готовых кирпичах: Leaflet, подложка из тайлов CARTO, поверх — Pixi с хитмапой. Убрал тайлы, оставил тёмный фон — и заодно с карты пропали границы государств (CARTO вжигает их прямо в картинку).

Единственные границы, которые тут нужны, — границы вкуса.
А потом Leaflet оказался вообще лишним: под капотом работала целая видеокарта, а управлял я ею через медленный слой DOM-трансформаций. Рендер переписал с нуля на голый WebGL2 — свои шейдеры, своя камера, своя проекция, триста строк на тач-жесты. А потом и Pixi, на котором держались точки, ушёл следом (гулять так гулять!)
Источник данных — OpenStreetMap: карту заведений там уже собрали добровольцы, и данные лежат открыто. Сначала забирал их через Overpass API (по одному блюду за раз), а когда блюд набралось много — решил просто скачать весь дамп планеты.
DuckDB разбирает его прямо из файла, без импорта в базенку. Одним запросом прохожу по всей планете и оставляю только общепит:
-- points: the node is its own coordinate
SELECT 'n'||id AS id, lat, lon, tags['name'] AS name, tags
FROM ST_ReadOSM('planet.osm.pbf')
WHERE kind = 'node'
AND tags['amenity'] IN ('restaurant', 'fast_food', 'cafe', 'food_court')
UNION ALL
-- polygons: take the coordinate of the outline's first node
SELECT 'w'||w.id, n.lat, n.lon, w.tags['name'], w.tags
FROM ST_ReadOSM('planet.osm.pbf') w
JOIN ST_ReadOSM('planet.osm.pbf') n ON n.id = w.refs[1]
WHERE w.kind = 'way'
AND w.tags['amenity'] IN ('restaurant', 'fast_food', 'cafe', 'food_court')Тонкость: в OSM заведение бывает либо точкой, либо контуром здания. Контуров — почти каждое пятое место (у фастфуда и фуд-кортов и того больше), так что «только точки» тихо теряли бы полмиллиона заведений. Поэтому беру и контуры: за координату сходит их первый узел — угол здания, а не геометрический центр, но на шаге кластеризации в 0.1° эта разница пропадает. Мультиполигоны-отношения всё же опускаю: их мало, а геометрию собирать долго.
Блюдо опознаёт обычный словарь регулярок, на каждое — две фазы. Широкая хватает всё подряд, лишь бы не пропустить: plov притащит и Plava Laguna, и кофейню Plowshares, а по «манты» однажды пришло 478 штук — с «романтикой» в комплекте (Romantic, Mantis, Manta). Узкая, с границами слова, всё это перепроверяет и отсекает — настоящих «манты» остаётся 154. Fuzzy-поиск здесь не помог бы: он утонул бы в тех же ложных срабатываниях, а узкой фазы для их очистки у него нет. Вот по каким написаниям карта узнаёт блюдо — на каждое их десятки:
Рис
- Плов
plovploffplofplowpalov- Пулао
pulaopullaopulavpolaopolaw- Бирьяни
biryanibiriyanibirianibriyanibriani- Паэлья
paellapaëllapaelyapaelleriapaellería- Ризотто
risottorissottorisotorizottoризотто
Пельмени и родня
- Пельмени
pelmenipelmenpelmeshpeljmenipielmieni- Вареники
varenikivarenykyvarenykвареникваренич- Pierogi
pierogipierogperogipyrohypierozki- Манты
mantimantımantymantuманты- Позы / буузы
позыпознаябуузыбуузбуузная
Шаурма / донер
- Шаурма / шаверма
shawarmashawermashaurmashoarmashwarma- Донер
dönerdonerdoenerdonairdönerci- Гирос
gyrosgyroyirosyeerosγύρος- Кебаб
kebabkebapкебабκεμπάπ케밥
Шашлык
- Шашлык
shashlikshashlykshaslikschaschlikšašlik- Шиш / сих-кебаб
shish kebabshish kabobseekh kebabsis kebapşiş- Сувлаки
souvlakisouvlakiasuvlakisouvlaσουβλάκι- Сатай
sataysatésatehsate ayam沙嗲- Якитори
yakitorikushiyaki焼き鳥焼鳥やきとり
Лапша
- Рамен
ramenラーメンらーめん拉麺中華そば- Фо
phởpho bopho gaфо боフォー- Удон
udonうどんウドン饂飩烏冬- Соба
sobasobaya蕎麦そば屋ソバ- Лагман
lagmanlaghmanлагманлағманلەغمەن
Шестая карта — про кухни, и устроена иначе. Здесь я смотрю не на название блюда, а на тег cuisine самого заведения. Разные теги свожу к одной из 29 национальных кухонь: sushi и ramen — японская, tapas — испанская. Глобальный фастфуд (pizza, burger) и мутные теги (asian) выкинул.
Разглядывая собранный словарь, я поймал у себя системную ошибку. Написаний у блюд накопилось неровно: у жареного риса двенадцать систем письма, у пельменей — латиница и кириллица, и всё. Значит, местами карта мерила не популярность блюда, а богатство его словарной графики. Пельменную в Таиланде под тайской вывеской она не видела в принципе.
Решение в лоб: для каждого блюда вытянуть максимальный набор имён из Wikidata (labels и aliases на всех языках) и положить поверх ручных регулярок. После нового прогона совпадений стало на 10,5% больше.
Самое интересное — ГДЕ вырос охват. Соба +257%: большинство лавок пишут そば хираганой, а старый словарь искал только кандзи. Сатай +118%. Дёнер зажёг всю Австралию. Перекос оказался азиатским: старый поиск хуже всего видел тайское письмо, китайские иероглифы, японские каны, корейский хангыль и деванагари. В Африке и Латинской Америке прирост был меньше — названия на латинице словарь в основном находил и раньше.
Расширение словаря подняло охват, но принесло ложные срабатывания: название блюда в одном языке может оказаться обычным словом в другом. Точность спасли два приёма: юникодная граница слова (JS-шный \b умеет только ASCII) и точечный блок-лист для смыслового дрейфа Wikidata: кофта ← кофта «одежда», момо ← モモ «персик». Проверка утечек нашла и более крупную: весь «прирост» перогов давала форма pirogi — осетинские пироги, то есть выпечка.
Самая красивая битва — кебаб. Синяя зона «шампурного кебаба» вдруг залила Германию, но там «Kebap» — это дёнер, а не иранский шампур. Одно слово, а смысл противоположный и по вывеске не различить. Ключ оказался в написании: kebab (через e) — дёнер-врап; kabab (через a) — шампур; kebap (через p) — пятьдесят на пятьдесят, поэтому его пришлось исключить совсем.
Пустое место лучше немецкого дёнера под видом иранского шампура.
Ещё два случая того же рода. Сочетание 餃子 в Японии читается как «гёдза», а в Китае и на Тайване — как «цзяоцзы». Без учёта языка оно превращало всю Японию в территорию цзяоцзы. Я развёл совпадения по маркерам: по умолчанию 餃子 относится к гёдза, но китайские контексты из этой категории вычитаются. Поэтому тайваньские шаньдунские пельменные остаются настоящими цзяоцзы.
С 点心 другая проблема: дим-сам — не отдельное блюдо, а кантонский формат еды, объединяющий десятки разных позиций. Поэтому я вынес его в отдельную категорию.
Из всей возни проступила общая картина: имя на вывеске врёт тремя способами. Общее слово с разным смыслом в разных регионах (кебаб) я не показываю. Общий иероглиф с разным чтением (餃子) отдаю доминирующему варианту, вычитая маркеры меньшинства. Общее имя вместо конкретного блюда (点心) получает собственную категорию-территорию. Правило одно: классифицирую по тому, что написано на вывеске. Ни рецепт, ни страна не в счёт.
После пересборки почти все сдвиги усилили тезис, ни один его не расшатал: шашлык стал строже, 68% → 97%.
Дальше из россыпи точек надо сделать области. Землю делю на мелкую сетку. Для каждой клетки нахожу ближайший ресторан, беру все рестораны вокруг него (радиус порядка градуса) и считаю, какого блюда среди них больше — в этот цвет клетку и крашу. Одинокий ресторан не заберёт целую область: решают соседи. По сути, это диаграмма Вороного по заведениям.
Отдельно повозился с фолбэками. Общие вывески вроде «Noodle House» или просто «Kebab» глушат конкретику — в Таиланде безликий noodle перебивал пад-тай. Поэтому обобщённые слова узнаю, но в раскраску не пускаю, и Таиланд снова читается как пад-тай.
В карточке каждого блюда — фотография из Wikimedia Commons; для каждой указаны автор и лицензия. Автоподстановка иногда подсовывала коллаж или старую картину вместо блюда, поэтому все фотографии я пересматривал контактным листом (собрал превью в одну простыню через ImageMagick), а неподходящие заменял руками.
Забавная деталь, которая рифмуется с находкой выше: у цзяоцзы и гёдзы одна и та же фотография — потому что в Википедии это одна статья.
Сейчас зоны считает пиксельный шейдер. А изначально хотелось иначе — посчитать Вороного один раз векторной геометрией и просто перерисовывать. Но d3-delaunay строит границы по своей метрике без учёта проекции карты, плюс зоны выходили угловатые и ломаные. В итоге с ним не срослось (хаха, не осилил).
Отдельная возня — с антимеридианом (линией перемены дат на 180°). Наивная карта считала расстояния в каждом полушарии отдельно: точка чуть западнее линии думала, что ближайший ресторан за две тысячи километров, хотя тот был в 300 километрах ЗА антимеридианом. Лечится просто — каждую затравку дублирую в копиях мира слева и справа, и долгота становится бесконечной, без шва.
Точки рисую инстансингом: один квадратик, три буфера на инстанс, одна команда на всё. Держу их в своём бинарном формате PB01 — вместо ~1,76 МБ JSON: дельты координат в zigzag-varint и байт «блюда» на точку. Декодируется примерно в 10 раз быстрее, чем JSON.parse().
Моя любимая грабля. После перехода на бинарник плотные регионы стали рисоваться уродливой регулярной сеткой. Данные декодились байт-в-байт идентично — никакой сетки в числах. Причина: рендер прореживает точки до одной на экранную ячейку, беря первую в порядке массива, а бинарник отсортирован пространственно ради сжатия — вот каждая «выжившая» точка и садилась в один угол ячейки. Починка: декодер детерминированно перемешивает точки перед рендером.
Порядок элементов в массиве был невидимой, но важной частью рендер-пайплайна.
Вороной уже разбил карту на территории блюд — а внутри каждой я ещё подсвечиваю по-разному: чем ближе к точкам-заведениям, тем светлее, вдали от них тускло. Так видно, где данных густо, а где карта рисует почти вслепую. Сначала суммировал вклад каждой точки и перебрал так с десяток вариантов: узкие гауссовы кляксы и конусы давали блямбы, а расширишь гаусс ради гладкости — он теряет пик и затухает в ноль. Суммирование — неправильная физика. Сработало поле расстояний: яркость задаёт не сумма, а расстояние до ближайшей точки (максимум, кольцами) — у точки светло, вдали тускло, ровно и без блямб. Считаю его на GPU алгоритмом Jump-Flood — линейно и одинаково по цене на любом зуме.
Это не тайлы и не внешняя подложка, а одна запечённая текстура высот, которую шейдер затеняет попиксельно прямо на экране. Высоты запёк из открытого DEM ETOPO 2022 (NOAA); шейдер сам считает отмывку — уклон по соседним текселям и освещение «солнцем» с северо-запада.

Дальше была сага. Готовая отмывка мылится при приближении — детали надо рассчитывать в разрешении экрана, а не сэмплить из картинки. Всю сушу хотелось уложить в 500 КБ, значит детали надо досочинять на лету — фрактальным шумом (ridged multifractal, fBm), искажая его вдоль настоящего склона (domain warp), чтобы синтетические хребты текли по реальному рельефу.
Всерьёз, кстати, обдумывал Wave Function Collapse. Отказался, потому что он дискретный, работает на CPU и в фиксированном разрешении — не умеет быть функцией на пиксель.
В процессе ужал террейн аж до 100 КБ — выглядело потрясающе, кроме береговой линии: пропадали острова, а Япония превращалась в гладкий банан. Поэтому в финале остановился на 500 КБ — там берега целы.
И ещё одна грабля — точность чисел. На части телефонов рельеф на большом зуме распадался в сетку квадратов, а на десктопе было чисто — вот это «на десктопе чисто» и стало подсказкой. Десктоп считает фрагменты в настоящих 32 битах (highp), а часть мобильных — в mediump (fp16), и изменение координаты на пиксель становится меньше доступной точности числа. Починка: определяю точность GPU на старте и гашу лишнюю детализацию только на затронутых устройствах.
Секрет 60 fps — одна идея: при движении карты не пересчитывать НИЧЕГО. Всё привязано к нормализованному Меркатору, поэтому пан и зум сводятся к репроекции. Тяжёлый пересчёт зон запускается, только когда зум устоялся. А наведение когда-то жгло 80% CPU над Европой (перебирало все заведения каждый кадр) — вынес ховер на отдельный 2D-canvas.
Визуальный язык шёл кривым путём. Первые версии выходили аскетичными — светлые тона, строгая типографика, — и всё выглядело уныленько. Поворот случился, когда я крутил переменные и увидел: карта выглядит как экран DEFCON — боевой планшет из командного штаба. Он и стал ориентиром.


Карта — ядро, но, чтобы ей пользовались, я достроил вокруг неё два инструмента. Режим истории: листаешь — карта сама едет по сюжету, меняя камеру, набор блюд и слои. Сначала читателя ведёт автопилот, а в конце отдаёт ему управление. Заголовки карт делал каламбурами: «Плов и Мир», «Ось шампура», «Найди вареник», «Вешая лапшу».
Конструктор карточек. Выбираешь кусок карты, формат (квадрат, 4:5, сторис 9:16, 16:9), правишь заголовок — и получаешь готовый PNG. Смысл — сделать читателя соавтором: дать ему карту как инструмент, из которого он соберёт собственную инфографику.
Тут лежит инструкция для ваших роботов: по ней они смогут воспроизвести весь проект.
Данные и оговорки
- OpenStreetMap — все заведения (© OpenStreetMap contributors, ODbL).
- Планетарный дамп OSM (~87 ГБ), из которого с помощью DuckDB извлечено 2,35 млн заведений.
- Wikidata — имена блюд (labels и aliases на всех языках) поверх ручного словаря регулярок.
- Wikimedia Commons — фотографии блюд; для каждой указаны автор и лицензия (CC BY / CC BY-SA / CC0 / PD).
- ETOPO 2022 (NOAA) — база высот для террейна.
Данные по миру распределены очень неравномерно: половина всех точек приходится на Западную Европу и Восточную Азию, хотя вместе они занимают около 5% суши. На тропическую Африку — доли процента. Поэтому надёжнее всего закономерности, которые сохраняются даже с учётом этого перекоса.
Карта видит только то, что написано в названиях заведений. Если блюдо не вынесли на вывеску, для карты его не существует. Границы между словами тоже не бывают идеально резкими: любая изоглосса расплывается по краям. Спорные написания вроде kebap я не учитывал, поэтому часть реальных заведений сюда не попала.
Что показывает работа
- GPU-рендеринг
- Свои шейдеры
- Геоданные планеты
- Мультиязычный NLP
- Картография
- Сторителлинг
- Продукт-дизайн
Собрано на
WebGL2Nuxt 3Vue 3DuckDBWikidataCloudflare PagesPlaywrightPython
Такой формат можно адаптировать для отчёта, исследования или спецпроекта на ваших данных.






