[{"data":1,"prerenderedAt":743},["ShallowReactive",2],{"page:\u002Fru\u002Fposts\u002Fcode_n_shit\u002Fcuando_tiles":3,"translations:\u002Fru\u002Fposts\u002Fcode_n_shit\u002Fcuando_tiles":721,"series:ru:cuando":726},{"id":4,"title":5,"author":6,"body":7,"date":707,"description":708,"draft":709,"extension":710,"image":711,"listed":348,"meta":712,"navigation":348,"path":713,"seo":714,"series":715,"stem":716,"tags":717,"theme":715,"toc":348,"__hash__":720},"content_ru\u002Fru\u002Fposts\u002Fcode_n_shit\u002Fcuando_tiles.md","Платим за каждый тайл","slnt_opp",{"type":8,"value":9,"toc":697},"minimark",[10,14,17,22,25,33,60,64,67,91,97,100,104,107,110,113,117,124,131,138,166,169,176,183,186,189,193,196,222,225,514,517,528,535,538,545,551,555,562,568,572,592,596,616,693],[11,12,13],"p",{},"С этой недели светлая и тёмная карты в Cuando — наши. Данные из OpenStreetMap, картинки рисует наш собственный сервер по нашим же стилям, а хранятся они в Cloudflare. Спутник по-прежнему берём у Mapbox. Если ты пользуешься приложением и ничего не заметил, так и было задумано.",[11,15,16],{},"Это история о том, как мы к этому пришли. Если оглянуться на последние полгода, каждое наше решение про карту вытекало из одного: как за карту брали деньги.",[18,19,21],"h2",{"id":20},"пять-дней-как-у-людей","Пять дней как у людей",[11,23,24],{},"Самой первой картой, ещё в августе, был фирменный Flutter SDK от Mapbox. Нативный, векторный, плавный: именно так карту в приложение и полагается встраивать.",[11,26,27,28,32],{},"Продержался он пять дней. Причины скучные. Плагин не работал в веб-сборке, где десктопные браузеры получали вежливое ",[29,30,31],"em",{},"«пока не поддерживается плагином карт»",", а мы хотим одну кодовую базу для Android, iOS и веба. Вдобавок сборке под Android нужен был специальный токен Mapbox для скачивания, а наш был не того типа.",[11,34,35,36,43,44,47,48,51,52,55,56,59],{},"Так что мы переехали на ",[37,38,42],"a",{"href":39,"rel":40},"https:\u002F\u002Fgithub.com\u002Ffleaflet\u002Fflutter_map",[41],"nofollow","flutter_map",". Он работает везде, потому что делает очень простую вещь: раскладывает сеткой квадратные картинки карты — их называют тайлами — и подгружает новые, пока ты двигаешь карту и зумишь. Картинки брали у Mapbox: стиль ",[29,45,46],{},"Navigation Day"," для светлой темы, ",[29,49,50],{},"Navigation Night"," для тёмной, плюс ",[29,53,54],{},"Streets"," и ",[29,57,58],{},"Satellite",".",[18,61,63],{"id":62},"арифметика","Арифметика",[11,65,66],{},"Через две недели мы сели считать и расписали всё в issue с предложением вернуться на SDK. Mapbox берёт деньги за SDK и за тайлы совершенно по-разному:",[68,69,70,82],"ul",{},[71,72,73,77,78,81],"li",{},[74,75,76],"strong",{},"Мобильный SDK"," оплачивается по числу активных пользователей за месяц, и первые ",[74,79,80],{},"25 000 в месяц"," бесплатны.",[71,83,84,87,88,81],{},[74,85,86],{},"Static Tiles"," — те самые картинки, которые показывал flutter_map, — оплачиваются по запросам, и первые ",[74,89,90],{},"200 000 запросов в месяц",[11,92,93,94,59],{},"Кажется, что запросов куча, пока не начнёшь их считать: каждый сдвиг и каждый зум требует целый экран свежих тайлов. Как написано в самом issue: ",[29,95,96],{},"«лимит Static Tiles в 200k запросов в месяц закончится раньше, чем у нас вообще появятся пользователи»",[11,98,99],{},"И это ещё был оптимистичный прогноз. Мы втроём одними тестами без труда выбирали всю бесплатную месячную квоту и с тех пор до этой самой недели платили Mapbox $10–30 в месяц за приложение, у которого почти не было пользователей. На SDK трое человек не стоили бы ничего. Но с SDK мы только что ушли, и по веским причинам. Тайлы вдвое крупнее (512 пикселей вместо 256, так что на экран их нужно вчетверо меньше) немного смягчили удар. А по-настоящему помочь могло только одно: перестать платить за каждый тайл.",[18,101,103],{"id":102},"неделя-когда-мы-попробовали-вектор","Неделя, когда мы попробовали вектор",[11,105,106],{},"Очевидное решение — векторные тайлы. Вместо картинок сервер отдаёт карту как данные — дороги, здания, подписи, — а рисует её уже сам телефон. Тайлы крошечные, смена стиля — это правка JSON, а подписи не заваливаются набок, когда крутишь карту.",[11,108,109],{},"В ноябре мы попробовали модуль векторных тайлов для flutter_map, только на мобильных (веб остался на картинках). Технически всё заработало на обеих платформах. На айфоне — без проблем. На Android карта лагала и дёргалась, и можно было смотреть, как она дорисовывается, тайл за тайлом.",[11,111,112],{},"Автобусное приложение открывают на остановке, с того телефона, какой уж есть, и многие из этих телефонов — Android-середнячки. Через неделю вектор выпилили.",[18,114,116],{"id":115},"не-рисуй-то-на-что-никто-не-смотрит","Не рисуй то, на что никто не смотрит",[11,118,119,120,123],{},"Значит, картинки. У картинок есть одно мерзкое свойство: на каждом уровне зума тайлов ",[74,121,122],{},"в четыре раза больше",", чем на предыдущем. Уровень улиц, который нужен большинству, — это примерно 18-й или 19-й зум, и именно там почти все тайлы.",[11,125,126,127,130],{},"Но большая часть нашего региона — это заросли кустарника, поля, два солёных озера (одно из них розовое) и море. Никому не нужно разглядывать солёное озеро на 19-м зуме. И запросить его тоже должно быть нельзя: каждый тайл, который ",[29,128,129],{},"можно"," запросить, — это тайл, который кому-то придётся скачать, оплатить или нарисовать.",[11,132,133,134,137],{},"Поэтому 12 февраля сервер начал считать ",[74,135,136],{},"сетку зума",". Это грубая сетка поверх региона, и в каждой ячейке лежит одно число: насколько глубоко там разрешено зумить. Правила простые:",[68,139,140,147,157,163],{},[71,141,142,143,146],{},"Везде для начала стоит ",[74,144,145],{},"зум 13",": этого хватает, чтобы увидеть, где какие города.",[71,148,149,150,153,154,59],{},"Ячейка, у которой в пределах пары километров от центра есть остановка, открывается до самого ",[74,151,152],{},"22",", а её соседи получают ",[74,155,156],{},"16",[71,158,159,160,59],{},"Ячейка, через которую проходит трасса маршрута, получает 16, а её соседи — около ",[74,161,162],{},"15",[71,164,165],{},"Всегда побеждает большее значение. Ячейку никогда не понижают.",[11,167,168],{},"В приложение сетка приезжает маленьким protobuf-файлом. Приложение смотрит, где сейчас центр экрана, и ограничивает зум по этой точке, интерполируя между соседними ячейками, чтобы лимит плавно скользил, а не прыгал. А ещё карту больше нельзя утащить за пределы региона.",[11,170,171],{},[172,173],"img",{"alt":174,"src":175},"Сетка зума в том виде, в каком она ушла в релиз: жёлтым — где можно зумить до упора, синим — вокруг, а всё остальное — z13. Длинный отросток на север — маршрут в аэропорт","\u002Fimages\u002Fcuando_tiles\u002Fzoom-grid-2025-ru.png",[11,177,178,179,182],{},"Это 54 × 72 ячейки примерно по 800 метров на площади около 2 560 км², и только 452 из 3 888 ячеек разрешают хоть что-то глубже зума 13. Если посчитать тайлы с 13-го по 19-й зум, у которых центр попадает туда, где сетка этот зум разрешает, выходит ",[74,180,181],{},"около 40 000 из 949 000",": примерно 4%, то есть в 24 раза меньше, чем вся пирамида.",[11,184,185],{},"Больше всего времени ушло на подкрутку: в каком радиусе от остановки можно зумить до упора, насколько широким делать коридор вдоль трассы и где проходит граница «достаточно» для городка, через который ты просто проезжаешь.",[11,187,188],{},"Без этой сетки следующий шаг был бы просто немыслим.",[18,190,192],{"id":191},"рисуем-сами","Рисуем сами",[11,194,195],{},"14 февраля мы взялись за собственный сервер карт. В первый же вечер в ход пошли сразу два подхода:",[68,197,198,213],{},[71,199,200,201,206,207,212],{},"стандартный набор: ",[37,202,205],{"href":203,"rel":204},"https:\u002F\u002Fgithub.com\u002Fsystemed\u002Ftilemaker",[41],"tilemaker",", чтобы нарезать данные OpenStreetMap на векторные тайлы, и ",[37,208,211],{"href":209,"rel":210},"https:\u002F\u002Fgithub.com\u002Fmaptiler\u002Ftileserver-gl",[41],"tileserver-gl",", чтобы их раздавать и рендерить из них картинки;",[71,214,215,216,221],{},"скрипт на сорок строк, который рендерил один тайл напрямую через ",[37,217,220],{"href":218,"rel":219},"https:\u002F\u002Fgithub.com\u002Fmaplibre\u002Fmaplibre-native",[41],"MapLibre Native",". tileserver-gl и сам под капотом рендерит на MapLibre Native.",[11,223,224],{},"tilemaker остался отвечать за данные. А вторую половину забрал скрипт. Через три дня он стал маленьким сервером на Express, а к концу недели его ядро в слегка урезанном виде выглядело вот так:",[226,227,232],"pre",{"className":228,"code":229,"language":230,"meta":231,"style":231},"language-js shiki shiki-themes monokai","app.get('\u002Ftiles\u002F:style\u002F:size\u002F:z\u002F:x\u002F:y', async (req, res) => {\n  \u002F\u002F …parse z, x, y and the size…\n  const center = mercator.ll([(x + 0.5) * 256, (y + 0.5) * 256], z)\n\n  const map = new maplibre.Map({ ratio: 1.0 })\n  map.load(styles[req.params.style])\n\n  map.render({ zoom: z, center, width: size, height: size }, (err, rgba) => {\n    map.release()\n    const png = new PNG({ width: size, height: size })\n    png.data.set(rgba)\n    res.set('Content-Type', 'image\u002Fpng')\n    png.pack().pipe(res)\n  })\n})\n","js","",[233,234,235,284,291,343,350,378,390,395,420,432,451,463,484,502,508],"code",{"__ignoreMap":231},[236,237,240,244,248,251,255,258,262,265,269,271,274,277,281],"span",{"class":238,"line":239},"line",1,[236,241,243],{"class":242},"sCdxs","app.",[236,245,247],{"class":246},"sHkqI","get",[236,249,250],{"class":242},"(",[236,252,254],{"class":253},"s_Ekj","'\u002Ftiles\u002F:style\u002F:size\u002F:z\u002F:x\u002F:y'",[236,256,257],{"class":242},", ",[236,259,261],{"class":260},"s8I7P","async",[236,263,264],{"class":242}," (",[236,266,268],{"class":267},"sW0Xf","req",[236,270,257],{"class":242},[236,272,273],{"class":267},"res",[236,275,276],{"class":242},") ",[236,278,280],{"class":279},"sOx1s","=>",[236,282,283],{"class":242}," {\n",[236,285,287],{"class":238,"line":286},2,[236,288,290],{"class":289},"snpHw","  \u002F\u002F …parse z, x, y and the size…\n",[236,292,294,297,300,303,306,309,312,315,319,321,324,327,330,332,334,336,338,340],{"class":238,"line":293},3,[236,295,296],{"class":279},"  const",[236,298,299],{"class":242}," center ",[236,301,302],{"class":260},"=",[236,304,305],{"class":242}," mercator.",[236,307,308],{"class":246},"ll",[236,310,311],{"class":242},"([(x ",[236,313,314],{"class":260},"+",[236,316,318],{"class":317},"s7s5_"," 0.5",[236,320,276],{"class":242},[236,322,323],{"class":260},"*",[236,325,326],{"class":317}," 256",[236,328,329],{"class":242},", (y ",[236,331,314],{"class":260},[236,333,318],{"class":317},[236,335,276],{"class":242},[236,337,323],{"class":260},[236,339,326],{"class":317},[236,341,342],{"class":242},"], z)\n",[236,344,346],{"class":238,"line":345},4,[236,347,349],{"emptyLinePlaceholder":348},true,"\n",[236,351,353,355,358,360,363,366,369,372,375],{"class":238,"line":352},5,[236,354,296],{"class":279},[236,356,357],{"class":242}," map ",[236,359,302],{"class":260},[236,361,362],{"class":260}," new",[236,364,365],{"class":242}," maplibre.",[236,367,368],{"class":246},"Map",[236,370,371],{"class":242},"({ ratio: ",[236,373,374],{"class":317},"1.0",[236,376,377],{"class":242}," })\n",[236,379,381,384,387],{"class":238,"line":380},6,[236,382,383],{"class":242},"  map.",[236,385,386],{"class":246},"load",[236,388,389],{"class":242},"(styles[req.params.style])\n",[236,391,393],{"class":238,"line":392},7,[236,394,349],{"emptyLinePlaceholder":348},[236,396,398,400,403,406,409,411,414,416,418],{"class":238,"line":397},8,[236,399,383],{"class":242},[236,401,402],{"class":246},"render",[236,404,405],{"class":242},"({ zoom: z, center, width: size, height: size }, (",[236,407,408],{"class":267},"err",[236,410,257],{"class":242},[236,412,413],{"class":267},"rgba",[236,415,276],{"class":242},[236,417,280],{"class":279},[236,419,283],{"class":242},[236,421,423,426,429],{"class":238,"line":422},9,[236,424,425],{"class":242},"    map.",[236,427,428],{"class":246},"release",[236,430,431],{"class":242},"()\n",[236,433,435,438,441,443,445,448],{"class":238,"line":434},10,[236,436,437],{"class":279},"    const",[236,439,440],{"class":242}," png ",[236,442,302],{"class":260},[236,444,362],{"class":260},[236,446,447],{"class":246}," PNG",[236,449,450],{"class":242},"({ width: size, height: size })\n",[236,452,454,457,460],{"class":238,"line":453},11,[236,455,456],{"class":242},"    png.data.",[236,458,459],{"class":246},"set",[236,461,462],{"class":242},"(rgba)\n",[236,464,466,469,471,473,476,478,481],{"class":238,"line":465},12,[236,467,468],{"class":242},"    res.",[236,470,459],{"class":246},[236,472,250],{"class":242},[236,474,475],{"class":253},"'Content-Type'",[236,477,257],{"class":242},[236,479,480],{"class":253},"'image\u002Fpng'",[236,482,483],{"class":242},")\n",[236,485,487,490,493,496,499],{"class":238,"line":486},13,[236,488,489],{"class":242},"    png.",[236,491,492],{"class":246},"pack",[236,494,495],{"class":242},"().",[236,497,498],{"class":246},"pipe",[236,500,501],{"class":242},"(res)\n",[236,503,505],{"class":238,"line":504},14,[236,506,507],{"class":242},"  })\n",[236,509,511],{"class":238,"line":510},15,[236,512,513],{"class":242},"})\n",[11,515,516],{},"Каждый запрос собирает новую карту, наводит окно в 512 пикселей на центр запрошенного тайла, рендерит и отдаёт PNG. MapLibre Native — это тот же C++-рендерер, что сидит внутри мобильных SDK MapLibre, и ему нужен OpenGL-контекст. У сервера экрана нет, поэтому всё это крутится в контейнере под Xvfb — виртуальным дисплеем.",[11,518,519,520,523,524,527],{},"Стили переделаны из ",[74,521,522],{},"Positron"," от OpenMapTiles для светлой темы и ",[74,525,526],{},"Dark Matter"," для тёмной. По дороге мы попробовали ещё несколько (OSM Bright, Liberty, один туристический, один ночной) и оставили Positron и Dark Matter: они специально сделаны так, чтобы тихо лежать под тем, что рисуешь сверху.",[11,529,530,531,534],{},"А теперь самое смешное. На вход рендерер получает ",[74,532,533],{},"векторную карту региона",", которую и так уже отдаёт наш собственный API: OpenStreetMap, который tilemaker нарезал на векторные тайлы. То есть у нас есть совершенно нормальная векторная карта и сервер, который превращает её в картинки, потому что телефоны не успевают рисовать её сами. Да, я в курсе.",[11,536,537],{},"Перед рендерером стоит Cloudflare Worker, и достучаться до рендерера может только он. Для каждого тайла он сначала заглядывает в R2, объектное хранилище Cloudflare. Если там пусто, он просит тайл у рендерера, отдаёт PNG телефону и по дороге кладёт копию в R2. Так что каждый тайл рисуется ровно один раз — для того, кто первым на него посмотрит.",[11,539,540,541,544],{},"Это важно, потому что рисовать долго. Железке, на которой крутится рендерер, нужно около ",[74,542,543],{},"трёх секунд"," на тайл, и одновременно она тянет штуки четыре. Первый, кто доберётся зумом до новой улицы, ждёт, а все после него — уже нет. С миллионом возможных тайлов ждать пришлось бы очень долго. С сорока тысячами — нормально.",[11,546,547],{},[172,548],{"alt":549,"src":550},"Светлая карта в приложении: снизу наши тайлы, сверху автобусные маршруты","\u002Fimages\u002Fcuando_tiles\u002Fapp-own-tiles.jpg",[18,552,554],{"id":553},"переезд","Переезд",[11,556,557,558,561],{},"Со стороны приложения переезд вышел почти скучным. И это нарочно: наши URL тайлов устроены так же, как у Mapbox, ",[233,559,560],{},"\u002F{style}\u002F512\u002F{z}\u002F{x}\u002F{y}@2x",", поэтому для flutter_map новая карта — это другой шаблон URL и другой токен доступа.",[11,563,564,565,567],{},"Теперь каждый стиль знает, откуда он берётся. Светлый и тёмный — наши. Спутник остаётся у Mapbox, потому что нам не из чего его рисовать. ",[29,566,54],{}," больше нет. А логотип Mapbox виден только тогда, когда ты смотришь на тайлы Mapbox. Последний кусок наш Flutter-разработчик выкатил незадолго до двух ночи — говорят, это традиционное время.",[18,569,571],{"id":570},"чего-это-стоит","Чего это стоит",[68,573,574,580,586],{},[71,575,576,579],{},[74,577,578],{},"Деньги:"," Mapbox, только за спутник. Хранилище R2 и запросы к Workers обходятся в копейки. Рендерер крутится на маленькой виртуалке, которая у нас и так была.",[71,581,582,585],{},[74,583,584],{},"Время:"," три секунды для первого, кто смотрит на тайл, который до него ещё никто не видел.",[71,587,588,591],{},[74,589,590],{},"Смена внешнего вида:"," вот она, настоящая цена. В картинку стиль запечён намертво. Поменять цвет — значит выкинуть все сохранённые тайлы и нарисовать их заново, и всё это дважды, потому что тем две.",[18,593,595],{"id":594},"что-дальше","Что дальше",[68,597,598,604,610],{},[71,599,600,603],{},[74,601,602],{},"Рисовать заранее,"," чтобы никто не ждал эти три секунды. Сетка зума точно говорит, какие тайлы рисовать.",[71,605,606,609],{},[74,607,608],{},"Логировать, какие тайлы люди реально запрашивают",", и пришли ли они из хранилища или их пришлось рисовать. Выкатываем завтра.",[71,611,612,615],{},[74,613,614],{},"Когда-нибудь — вектор."," На сервере карта уже векторная. Осталось уговорить телефоны.",[617,618,619,625],"note",{},[11,620,621,624],{},[74,622,623],{},"Из сентября 2026."," Этот пост датирован неделей, когда приложение переехало на наши тайлы, и написан так, как всё выглядело тогда. С высоты девятнадцати месяцев:",[68,626,627,633,639,653,669,675,681,687],{},[71,628,629,632],{},[74,630,631],{},"По времени всё сошлось."," К тому моменту, когда мы всерьёз начали рассказывать людям про Cuando, все карты в нём, кроме спутника, были наши.",[71,634,635,638],{},[74,636,637],{},"Картинки никуда не делись."," Flutter-приложение до сих пор рисует наши растровые тайлы, и сетка зума по-прежнему едет вместе с ним. Теперь её генерирует API, на площади побольше, и в нашей админке у неё появилась своя вкладка специально для подкрутки.",[71,640,641,644,645,648,649,652],{},[74,642,643],{},"Рисовать заранее начали в апреле 2026 года."," Зумы с 13-го по 19-й в прямоугольнике сетки — это ",[74,646,647],{},"944 382 тайла на тему",". Виртуалке понадобилось бы около восьми дней на тему, поэтому пререндер крутится на моём Mac mini: ",[74,650,651],{},"от 10 до 12 часов на тему, около 15 ГБ на каждую",". Виртуалка дорисовывает только то, чего не хватает.",[71,654,655,658,659,664,665,668],{},[74,656,657],{},"Вектор всё равно победил, просто не во Flutter."," Весь регион одним векторным архивом (файл ",[37,660,663],{"href":661,"rel":662},"https:\u002F\u002Fdocs.protomaps.com\u002Fpmtiles\u002F",[41],"PMTiles",") весит ",[74,666,667],{},"58,67 МБ",". Это примерно в 255 раз меньше одной растровой темы, и этого одного архива хватает на все темы сразу: новая тема — это файл стиля на 22 КБ. Worker раздаёт его прямо из R2 и вычитывает для каждого тайла только нужные байты. Железка для рендера тут вообще не нужна. Глубже максимального зума архива телефон просто растягивает векторные данные, так что векторной карте и сетка зума не нужна.",[71,670,671,674],{},[74,672,673],{},"Где он используется:"," наша админка переехала на MapLibre в мае 2026 года. Новые приложения, переписанные на Swift со Skip, рисуют векторные карты: на Android — MapLibre на наших тайлах, на iOS — MapKit от Apple.",[71,676,677,680],{},[74,678,679],{},"Во Flutter вектор попробовали ещё раз"," в 2026-м, с включённым GPU-рендерингом. На Redmi-середнячке всё сразу же упало. Это уже история для второго поста про карты.",[71,682,683,686],{},[74,684,685],{},"Лимит зума так и не заскользил."," Приложение переводит позицию в целые номера ячеек ещё до того, как «интерполировать» между ними, так что всегда читает ровно одну ячейку, и лимит всё-таки прыгает. Полтора года этого никто не замечал.",[71,688,689,692],{},[74,690,691],{},"Спутник"," — последнее, что мы брали у Mapbox, — убрали из выбора карт в августе 2026 года.",[694,695,696],"style",{},"html pre.shiki code .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .sHkqI, html code.shiki .sHkqI{--shiki-default:#A6E22E}html pre.shiki code .s_Ekj, html code.shiki .s_Ekj{--shiki-default:#E6DB74}html pre.shiki code .s8I7P, html code.shiki .s8I7P{--shiki-default:#F92672}html pre.shiki code .sW0Xf, html code.shiki .sW0Xf{--shiki-default:#FD971F;--shiki-default-font-style:italic}html pre.shiki code .sOx1s, html code.shiki .sOx1s{--shiki-default:#66D9EF;--shiki-default-font-style:italic}html pre.shiki code .snpHw, html code.shiki .snpHw{--shiki-default:#88846F}html pre.shiki code .s7s5_, html code.shiki .s7s5_{--shiki-default:#AE81FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":231,"searchDepth":286,"depth":286,"links":698},[699,700,701,702,703,704,705,706],{"id":20,"depth":286,"text":21},{"id":62,"depth":286,"text":63},{"id":102,"depth":286,"text":103},{"id":115,"depth":286,"text":116},{"id":191,"depth":286,"text":192},{"id":553,"depth":286,"text":554},{"id":570,"depth":286,"text":571},{"id":594,"depth":286,"text":595},"2025-02-22","Все решения про карту в Cuando до сих пор вытекали из того, как за эту карту брали деньги. Полгода Mapbox, одна неделя векторных тайлов, тепловая карта, которая решает, насколько глубоко можно зумить, и — с этой недели — карта, которую мы рисуем сами.",false,"md","\u002Fimages\u002Fcuando_tiles\u002Fcover.jpg",{},"\u002Fru\u002Fposts\u002Fcode_n_shit\u002Fcuando_tiles",{"title":5,"description":708},"cuando","ru\u002Fposts\u002Fcode_n_shit\u002Fcuando_tiles",[233,715,718,719],"maps","cloudflare","F_8ROSmTXRlJPAexBlFbwrCiQfwTjPHRZgTWCODfXec",[722],{"locale":723,"path":724,"title":725},"en","\u002Fposts\u002Fcode_n_shit\u002Fcuando_tiles","Paying per tile",{"slug":715,"title":727,"path":728,"chapters":729},"Как мы делали Cuando","\u002Fru\u002Fseries\u002Fcuando",[730,736,737],{"path":731,"title":732,"description":733,"date":734,"image":735,"draft":709,"series":715,"n":239},"\u002Fru\u002Fposts\u002Fcode_n_shit\u002Fcuando_bus_app","Мы сделали приложение про автобусы Торревьехи. Оно задаёт один вопрос","Cuando — приложение про автобусы Торревьехи, теперь в Google Play и App Store. Данных для старта не было, так что сеть мы нарисовали руками: 163 остановки, 16 маршрутов, приложение-секундомер и куча YAML. Рассказываю, как оно работает и почему графовая база продержалась восемь недель.","2025-01-24","\u002Fimages\u002Fcuando_bus_app\u002Fcover.jpg",{"path":713,"title":5,"description":708,"date":707,"image":711,"draft":709,"series":715,"n":286},{"path":738,"title":739,"description":740,"date":741,"image":742,"draft":709,"series":715,"n":293},"\u002Fru\u002Fposts\u002Fcode_n_shit\u002Fcuando_vector","Пятьдесят восемь мегабайт","Полтора года мы рисовали карту картинками. Почему Cuando переезжает на вектор: один файл на 58,67 МБ вместо 15 ГБ на тему, упавший рендерер во Flutter и три картографических движка за одной вьюхой в новых приложениях.","2026-08-15","\u002Fimages\u002Fcuando_vector\u002Fcover.jpg",1790723721541]