<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[MeetAndPlay Engineering]]></title><description><![CDATA[MeetAndPlay Engineering]]></description><link>https://meetandplay.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 10:02:35 GMT</lastBuildDate><atom:link href="https://meetandplay.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a nationwide court directory for Mexico from OpenStreetMap]]></title><description><![CDATA[We needed a list of every place to play padel, tennis and pickleball in Mexico. Nobody sells a good one, so we built it from OpenStreetMap, and learned…]]></description><link>https://meetandplay.hashnode.dev/building-a-nationwide-court-directory-for-mexico-from-openstreetmap</link><guid isPermaLink="true">https://meetandplay.hashnode.dev/building-a-nationwide-court-directory-for-mexico-from-openstreetmap</guid><category><![CDATA[openstreetmap]]></category><category><![CDATA[#OpenData]]></category><dc:creator><![CDATA[MeetAndPlay]]></dc:creator><pubDate>Thu, 05 Nov 2026 18:02:08 GMT</pubDate><content:encoded><![CDATA[<p><em>We needed a list of every place to play padel, tennis and pickleball in Mexico. Nobody sells a good one, so we built it from OpenStreetMap, and learned that an open map is a mirror of the people who edited it.</em></p>
<p>MeetAndPlay is a padel, tennis and pickleball app that launches in Cancún on 1 December 2026. An app that helps people find a game needs to know where the courts are, in a country with 32 states and a lot of places that never made it into a booking platform. We built a nationwide directory, the <a href="https://meetandplay.mx/canchas">court directory at meetandplay.mx/canchas</a>, on top of OpenStreetMap. This post covers the pipeline, the licence, and the quality problems we found, with the real numbers from our latest snapshot.</p>
<h2>Why OpenStreetMap</h2>
<p>The alternatives were scraping a booking marketplace, buying a commercial POI dataset, or using a map provider's places API. We rejected each for a different reason: terms of service, price, and licences that forbid storing and re-publishing the data. OpenStreetMap is the only source that lets you pull the data, keep it, build pages from it, and re-publish what you derive, provided you follow the Open Database Licence (ODbL 1.0) and credit contributors.</p>
<h2>The pipeline</h2>
<p>The pipeline is a plain script, run by hand when we want a refresh. There is no scheduler and no live dependency on the Overpass API at request time.</p>
<ol>
<li><strong>Pull.</strong> Query the Overpass API for features tagged with a racket sport (<code>sport=tennis</code>, <code>sport=padel</code>, <code>sport=pickleball</code>). Our latest pull has the snapshot timestamp <code>2026-09-25T20:14:17Z</code>. The timestamp travels with the data, so every page and every number can say how old it is.</li>
<li><strong>Filter.</strong> The most important step. Tennis courts are everywhere in OSM: schools, universities, apartment complexes. We keep facilities that make sense as a place to play and exclude school and university campuses. One filter function is shared by every consumer, because two copies of "what counts as a venue" drift apart within a week.</li>
<li><strong>Locate.</strong> Assign each place to a state and municipality from OSM's administrative boundaries, matched against INEGI's municipality list. Reverse-geocode a colonia and street with Nominatim, because an OSM point alone is not an address. Compute the distance to a coastline from Natural Earth, which turns out to be useful on a beach-heavy market.</li>
<li><strong>Sanitize.</strong> OSM contact tags are free text edited by anyone. A "website" value can be a <code>javascript:</code> URL; a "phone" can have no digits. We drop anything that cannot be a safe link at build time, and the render layer checks again.</li>
<li><strong>Aggregate.</strong> Write state and city indexes as generated TypeScript modules. Static data means no database call to render a directory page.</li>
</ol>
<p>The rule that guided everything: <strong>no invented data.</strong> If OSM has no name for a place, the page gets no name. We label it from its sport and colonia at render time instead of guessing. That sounds like a small thing and it prevents the whole class of bug where an app confidently shows a made-up club.</p>
<h2>What the snapshot says</h2>
<p>Counting everything in the snapshot, OSM has 1,052 places across all 32 states and 255 cities, carrying a total of 2,307 courts. Only 354 places have a name, a third of the total.</p>
<p>Per sport, counted as <em>places carrying that sport tag</em>:</p>
<table>
<thead>
<tr>
<th>Sport</th>
<th align="right">Places in OSM</th>
</tr>
</thead>
<tbody><tr>
<td>Tennis</td>
<td align="right">1,005</td>
</tr>
<tr>
<td>Padel</td>
<td align="right">51</td>
</tr>
<tr>
<td>Pickleball</td>
<td align="right">14</td>
</tr>
</tbody></table>
<p>A caution that cost us an embarrassing correction in draft: OSM counts places per sport, not courts per sport. A club with a padel court and two tennis courts is one place in the padel column and one in the tennis column. So our tables say "places with padel", never "padel courts". The 2,307 courts figure is the total across every sport and cannot be split.</p>
<p>Two results made us rethink the directory:</p>
<ul>
<li><strong>Padel is almost invisible on the open map.</strong> 51 places in the whole country is far below what the sport's own federation says exists. The International Padel Federation published, in March 2025, "more than 630 clubs" and 2,300 courts for Mexico. Those are their figures, from their research department, and we label them directional: we cannot audit them and we do not divide one number by the other. But the gap exists, and it is the finding.</li>
<li><strong>Cancún looks like a tennis town on OSM.</strong> The map has 35 places and 58 courts in Cancún, all of them tagged tennis, none tagged padel, and none with a name. A city where padel is the headline sport has no padel in OSM. By state, the places with padel are concentrated where a few mappers care: Yucatán 8, Sonora 7, Coahuila 6, Jalisco 5. Quintana Roo, which has 71 places, has 0.</li>
</ul>
<p>An open map reflects who edits it. That is the lesson to carry to any OSM-derived project: your coverage is a coverage of mappers, not of the world.</p>
<h2>The licence, in practice</h2>
<p>I am not a lawyer and this is not legal advice, but here is how we handled ODbL, which is the thing people get wrong.</p>
<ul>
<li><strong>Attribution</strong> on every page that uses the data, as "© OpenStreetMap contributors", linked to the licence. No exceptions for a footer you can't see.</li>
<li><strong>Share-alike</strong> applies to derived <em>databases</em>. Anything we publish as data (our per-state CSV, our dataset markup) is offered under ODbL with the same credit.</li>
<li><strong>Keep the licensed data separate from data that isn't.</strong> We also research venues by hand from public sources, each row citing its source URL. Those rows are <em>not</em> OSM's and are not ODbL. If we merged them into the same published dataset, we would either relicense them or contaminate the OSM-derived release. So the published report and CSV read <strong>only</strong> the OSM-derived modules. The directory's own totals, which also count the hand-researched venues, never appear in the ODbL-labelled outputs. In code this is the difference between two exported objects, and a comment in the module saying not to mix them.</li>
</ul>
<h2>Publishing the result</h2>
<p>Because the dataset is ODbL, we put it out as a <a href="https://meetandplay.mx/reporte-padel-mexico">report with a downloadable CSV</a>: per-state places, courts, and places per sport, plus the methodology and the caveats above. The CSV has Spanish snake_case column names and is served from a path with a dot in it on purpose, so our locale-rewriting middleware leaves it alone and it is the same in every language.</p>
<p>All numbers on the page are derived from the same module that builds the CSV. We never type a figure by hand. If we re-pull OSM, the page, the file and the structured data move together.</p>
<h2>Quality problems you will hit</h2>
<ul>
<li><strong>Duplicates.</strong> The same club mapped as a point, a polygon and a relation. Dedupe by proximity and name, not by ID.</li>
<li><strong>Wrong sport tags.</strong> A tennis court re-lined for pickleball keeps the tennis tag. Do not infer sport counts beyond what is tagged.</li>
<li><strong>Closed and abandoned places.</strong> OSM has lifecycle prefixes, and not everyone uses them. We say clearly that the directory is what mappers recorded, not what any club verified.</li>
<li><strong>Municipality edges.</strong> Border points flip between adjacent municipalities in different data vintages.</li>
</ul>
<h2>What we would tell you to do</h2>
<ol>
<li>Version your snapshot and print its timestamp wherever a number appears.</li>
<li>Decide what counts as a venue once, in one function.</li>
<li>Never mix licences in one published file.</li>
<li>Treat a gap between OSM and an outside source as a feature of your report, not a bug in your pipeline. That is also an invitation: if you play in Cancún and you know where the padel courts are, adding them to OpenStreetMap makes the map better for everyone, not just us.</li>
</ol>
<p>If you want to poke at the data, the CSV is public, the code is in the <a href="https://github.com/idimetrix/canchas-mexico">canchas-mexico repo on GitHub</a>, and we would like to hear what you build from it.</p>
<p><em>Data © OpenStreetMap contributors, ODbL 1.0. Snapshot 2026-09-25T20:14:17Z. External figure: FIP, "Focus en… México", 11 March 2025.</em></p>
]]></content:encoded></item><item><title><![CDATA[Shipping a sports app in Nahuatl, Maya, Tzeltal and Tzotzil: i18n lessons from 60 locales]]></title><description><![CDATA[We build MeetAndPlay, a padel, tennis and pickleball matchmaking and court-booking app that launches in Cancún on 1 December 2026. Here is what we learned…]]></description><link>https://meetandplay.hashnode.dev/shipping-a-sports-app-in-nahuatl-maya-tzeltal-and-tzotzil-i18n-lessons-from-60-locales</link><guid isPermaLink="true">https://meetandplay.hashnode.dev/shipping-a-sports-app-in-nahuatl-maya-tzeltal-and-tzotzil-i18n-lessons-from-60-locales</guid><category><![CDATA[i18n]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[webdev]]></category><category><![CDATA[localization]]></category><dc:creator><![CDATA[MeetAndPlay]]></dc:creator><pubDate>Thu, 05 Nov 2026 18:00:58 GMT</pubDate><content:encoded><![CDATA[<p><em>We build MeetAndPlay, a padel, tennis and pickleball matchmaking and court-booking app that launches in Cancún on 1 December 2026. Here is what we learned adding indigenous Mexican languages to a Next.js app, including the parts that are still unfinished.</em></p>
<p>When we started, the plan was ordinary: Spanish first, English for tourists, a handful of European languages for the people who fly into Cancún. Then a directive changed the scope: support every language that is actually popular in Mexico. Taken literally, that does not mean French and Dutch. It means Nahuatl, Yucatec Maya, Tzeltal, Tzotzil and a long tail after them.</p>
<p>The locale list started smaller and has grown to 60 tags today, so treat any number in this post as a moving target. This post is about the engineering and the judgment calls, written for anyone who has to ship a language that your tooling does not know exists. It is also about what we have not done, because that is the part most i18n posts skip.</p>
<p>If you want to see the result, the <a href="https://meetandplay.mx/en">MeetAndPlay site</a> has a language switcher where the indigenous languages sit in their own group.</p>
<h2>Why these languages at all</h2>
<p>The criterion we ended up with is demographic, and it comes from Mexico's statistics institute. INEGI's 2020 census counted 7,364,645 people aged three and over who speak an indigenous language, across 68 languages. About 866,000 of them (11.8%) do not speak Spanish. Nahuatl has 1,651,958 speakers, Maya (Yucatec) 774,755, Tseltal 589,144 and Tsotsil 550,274.</p>
<p>For a Cancún product, two of those matter for a reason beyond the national numbers. Yucatec Maya is native to Quintana Roo. And in our own research notes, Tseltal and Tsotsil came out as the second and third most spoken indigenous languages in Cancún after Maya (4,098 and 3,208 speakers in the Benito Juárez municipality), with the highest monolingual rates of the four. Those are Chiapas migrants who work in hospitality, including at sports clubs. For them a Spanish-only screen may be no screen at all.</p>
<p>We are not claiming these readers are our customers today; a product built for a Mexican city should simply be usable by the people who live in it.</p>
<h2>Lesson 1: locale tags are a permanent public decision</h2>
<p>A locale tag ends up in four places you cannot easily change: the URL segment, <code>&lt;html lang&gt;</code>, <code>hreflang</code>, and Open Graph's <code>og:locale</code>. Once other sites link to <code>/xyz/</code>, renaming it costs real money. So we audited every tag against two registers, the ISO 639-3 code set and the IANA language subtag registry, before shipping any copy. Three findings, in increasing order of annoyance:</p>
<p><strong>Some languages map to one code cleanly.</strong> <code>yua</code> (Yucatec Maya), <code>tzh</code> (Tzeltal) and <code>tzo</code> (Tzotzil) are individual languages with one code each. Easy.</p>
<p><strong>Nahuatl has no individual code.</strong> <code>nah</code> is a <em>collection</em> code: ISO 639-3 splits Nahuatl into about 29 individual languages and none of them is "Nahuatl". RFC 5646 says a collection subtag may be used when more specific information is unavailable, but that it SHOULD NOT be used for content better tagged with an individual language. We kept <code>nah</code> anyway, because picking one variety (say, the Huasteca one) would tell a reader from Guerrero that we chose someone else's Nahuatl for them. The cost is real and we document it: a browser sending <code>Accept-Language: nhe</code> will not match <code>nah</code>. Content negotiation is where this bites.</p>
<p><strong>One of our tags was simply the wrong language.</strong> <code>mix</code> looks like "Mixtec" but ISO 639-3 defines it as Mixtepec Mixtec, the speech of one municipality in Oaxaca, with on the order of ten thousand speakers. Mixtec as a whole has over half a million speakers in the census and 52 individual codes with no macrolanguage above them. If we ever served <code>&lt;html lang="mix"&gt;</code> for a translated Mixtec site, we would tell screen readers and search engines something false about roughly 98% of the intended readers. It is still open as a decision; the lesson is that a tag that "looks right" is not a tag that has been checked.</p>
<p>A smaller one: ISO prints "Tzeltal" and "Tzotzil"; the modern INALI orthography is "Tseltal" and "Tsotsil". We show the latter in the switcher and keep the ISO codes in the markup.</p>
<h2>Lesson 2: your date and number libraries do not know these languages</h2>
<p>Not one indigenous tag we ship has CLDR date or number data, and only Nahuatl has plural rules. <code>Intl.DateTimeFormat("tzh")</code> falls back silently, usually to the runtime default, which is not what you want. We route every date and number through a <code>formattingLocale()</code> helper that maps each locale to the nearest language that has data. For these audiences that means formatting like Mexican Spanish. It is a pragmatic answer, not a right one; native speakers should decide how a date reads.</p>
<p>Plural rules are the quieter problem: for a language with no CLDR rules the engine falls back to a default that may be wrong, so we avoid plural-sensitive phrasing where we can, and our check script treats broken ICU syntax as a hard error.</p>
<h2>Lesson 3: honest status beats a complete-looking list</h2>
<p>This is the part we most want other teams to copy. The tempting move is to generate a catalogue for every language and ship it. A machine-drafted sentence in a language the model barely knows can be confidently wrong, and the reader cannot tell. So each locale has an explicit status:</p>
<ul>
<li><strong>reviewed</strong>: a human who reads the language checked the catalogue. Seven locales: Spanish, English, Portuguese, French, German, Italian, Russian.</li>
<li><strong>machine</strong>: generated and not reviewed by a native speaker. It carries a "beta" badge in the switcher. Nineteen locales are here, including Chinese, Korean, Japanese, Dutch, Arabic, Yucatec Maya, Nahuatl, Tzeltal and Tzotzil.</li>
<li><strong>pending</strong>: the file exists, but its contents are a structural copy of the fallback language. For a Mexican indigenous language the fallback is Spanish, because those readers already navigate in Spanish, and a Spanish screen is more honest than invented text.</li>
</ul>
<p>Thirty-four indigenous catalogues are in the pending state today (7 reviewed + 19 machine + 34 pending = 60). They are in the list because the directive was to cover the languages, and they are <code>pending</code> because we would rather say so than pretend.</p>
<p>The part that matters for engineering is that "pending" is <em>derived</em>, not hand-flagged. A pending locale renders <code>&lt;html lang="es-MX"&gt;</code>, is excluded from <code>hreflang</code> and the sitemap, gets <code>noindex</code>, and reports <code>og:locale</code> as <code>es_MX</code>, so the markup never claims a language the page does not contain. The trap: the moment someone translates a pending file, the whole <code>lang</code>/<code>hreflang</code>/sitemap cluster switches on automatically, and nobody re-decides the tag. That is why the <code>mix</code> problem matters now, while it is cheap to fix.</p>
<h2>Lesson 4: make the build enforce the boring invariants</h2>
<p>You cannot review 60 catalogues by eye. Our check script treats key parity, placeholder parity (<code>{count}</code>, <code>{club}</code>) and valid ICU syntax as hard errors in every locale. For pending locales it downgrades only two rule families to warnings: "identical to another locale" and CJK script purity. That lets us hold Spanish text in a Tzeltal file without failing the build, while still failing a catalogue that drops a key or a placeholder.</p>
<p>One pitfall worth stealing: surfaces like our OG cards and the WhatsApp share layer once kept their own hand-maintained list of pending locales, and went stale on every promotion. We now derive every such list from the single status file and pin it with a test.</p>
<h2>What we have not done</h2>
<p>Being straight about this, because writing "60 locales" in a headline invites a different reading than the truth.</p>
<ul>
<li><strong>No native-speaker review yet</strong> for any indigenous locale. The status is machine, and it says beta on the screen. Finding reviewers is the actual work.</li>
<li><strong>Dozens of locales are Spanish text in disguise.</strong> They are a commitment and a structure, not a finished product.</li>
<li><strong>Not every language is covered.</strong> We excluded languages when ISO 639-3 splits them into many coordinate varieties with no obvious standard, and later accepted that risk for some by choosing the largest variety, with a requirement that a native reviewer confirms it before promotion. That decision can be wrong, and we say so in the repo.</li>
<li><strong>We have no usage numbers for these locales,</strong> and will not make any up.</li>
</ul>
<p>If you are a speaker of Nahuatl, Yucatec Maya, Tseltal or Tsotsil and want to correct our strings, we would like to hear from you at <a href="mailto:hola@meetandplay.mx">hola@meetandplay.mx</a>.</p>
<h2>Takeaways</h2>
<p>Audit tags against ISO 639-3 and IANA before building on them; keep each catalogue's status in one file and derive everything else from it; prefer a labeled fallback to confident gibberish; and test keys, placeholders and ICU in every locale. The app is <a href="https://meetandplay.mx/canchas">MeetAndPlay</a>: find a game, find players, book a court.</p>
<p><em>Sources: INEGI, Censo de Población y Vivienda 2020 (press release 430/22); ISO 639-3 code set (SIL); IANA Language Subtag Registry; RFC 5646.</em></p>
]]></content:encoded></item></channel></rss>