<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: jiang Wang</title>
    <description>The latest articles on DEV Community by jiang Wang (@sinostay).</description>
    <link>https://dev.to/sinostay</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4225614%2F81d88d67-5c70-4ddd-aac8-d8a063c91f84.png</url>
      <title>DEV Community: jiang Wang</title>
      <link>https://dev.to/sinostay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sinostay"/>
    <language>en</language>
    <item>
      <title>A name is not an identifier: what 1,269 Chinese railway stations taught me about labelling</title>
      <dc:creator>jiang Wang</dc:creator>
      <pubDate>Thu, 05 Nov 2026 08:47:13 +0000</pubDate>
      <link>https://dev.to/sinostay/a-name-is-not-an-identifier-what-1269-chinese-railway-stations-taught-me-about-labelling-fei</link>
      <guid>https://dev.to/sinostay/a-name-is-not-an-identifier-what-1269-chinese-railway-stations-taught-me-about-labelling-fei</guid>
      <description>&lt;p&gt;I spent a week loading Chinese railway stations into a database, and the interesting part turned out to be a modelling problem rather than a scraping one.&lt;/p&gt;

&lt;p&gt;The symptom, in one sentence: the label a user sees and the key you store are different things, and in this dataset they diverge in a way that ruins an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The set
&lt;/h2&gt;

&lt;p&gt;1,269 published railway stations across 340 cities. 262 airports across 212 cities. Every row carries a Chinese name, an English name, a city, coordinates.&lt;/p&gt;

&lt;p&gt;The distribution is not what I expected. Beijing leads with 50 stations. Harbin has 40. Chengde — a small city most people only pass through on the way to the Qing tombs — has 26. Then Chengdu 30, Guiyang 24, Hulunbuir 21. 205 of the 340 cities have more than one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 1: the Chinese name is unique, the English name is not useful
&lt;/h2&gt;

&lt;p&gt;The first query I ran was a duplicate check, expecting a mess:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;railways&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero rows. All 1,269 published station names are distinct across the entire country. That is a stronger guarantee than most geographic datasets offer — usually you find at least a handful of collisions, and you need an administrative prefix to disambiguate.&lt;/p&gt;

&lt;p&gt;So the Chinese string is a legitimate natural key. Good.&lt;/p&gt;

&lt;p&gt;But the English name fails for a different reason. It is not duplicated — it is &lt;em&gt;unactionable&lt;/em&gt;. Consider 北京南站. The romanisation is &lt;code&gt;Beijingnan&lt;/code&gt;. That is what appears on the ticket, on the departure board, and in every app. What a person says at a taxi rank is "Beijing South". What the driver reads is neither: the driver reads 北京南站 off a sign, or nothing at all.&lt;/p&gt;

&lt;p&gt;You end up with three representations of one station, and only one of them can be compared against a physical sign in the street. The English one, which is what a foreign visitor instinctively reaches for, is the least useful of the three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 2: a third of the set is a compass direction
&lt;/h2&gt;

&lt;p&gt;Counting the suffix patterns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Suffix&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;南 (south)&lt;/td&gt;
&lt;td&gt;124&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;北 (north)&lt;/td&gt;
&lt;td&gt;123&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;东 (east)&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;西 (west)&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;452 of 1,269 — 35.6% — are a city name plus a compass point. The remaining two-thirds are district names, town names, or standalone names that happen to be a station. And 301 stations are exactly the bare &lt;code&gt;city + 站&lt;/code&gt; form, which is the historical central terminus: 北京站, 天津站, 广州站.&lt;/p&gt;

&lt;p&gt;That last figure is the trap. If a city has a bare-name station &lt;em&gt;and&lt;/em&gt; compass variants, the bare name is the one a visitor will guess, and it is frequently not the one their train departs from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 3: 36 cities have more than one airport
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;city_slug&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;airports&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;city_slug&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- 36&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shanghai's two airports are 45 km apart. Chengdu's are on opposite sides of the metro area. The failure mode is identical to the station one, and it is worse because the English names (Pudong, Hongqiao) look like proper nouns rather than positions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed in the schema
&lt;/h2&gt;

&lt;p&gt;Three decisions, all boring, all consequential:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Chinese string is the display value and the natural key.&lt;/strong&gt; It is the only representation that survives contact with a taxi rank.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The English name is retained for full-text search only.&lt;/strong&gt; It never appears as the sole label anywhere in the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coordinates are mandatory, not optional.&lt;/strong&gt; A station page without a map pin is a page that cannot resolve the ambiguity it exists to resolve.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third rule removed 0 rows, because the upstream source had coordinates for everything — but writing it down as a constraint is what stopped me from ever building a station page without one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;If your dataset's user-facing label is also your join key, you will at some point join two different things that read the same. Here the Chinese name happens to be both unique and unambiguous, which is lucky. The English name is unique too, and still manages to be the wrong thing to show someone standing at a kerb.&lt;/p&gt;

&lt;p&gt;Uniqueness and usefulness are separate properties. Only one of them is visible in a &lt;code&gt;GROUP BY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The dataset and the per-station pages are browsable at &lt;a href="https://sinostay.com/" rel="noopener noreferrer"&gt;sinostay&lt;/a&gt; — the station list itself is at &lt;a href="https://sinostay.com/traffic/railways/" rel="noopener noreferrer"&gt;/traffic/railways/&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>data</category>
      <category>database</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>China in July: 27 cities are comfortable. In May, 255 are.</title>
      <dc:creator>jiang Wang</dc:creator>
      <pubDate>Thu, 05 Nov 2026 03:06:32 +0000</pubDate>
      <link>https://dev.to/sinostay/china-in-july-27-cities-are-comfortable-in-may-255-are-4i49</link>
      <guid>https://dev.to/sinostay/china-in-july-27-cities-are-comfortable-in-may-255-are-4i49</guid>
      <description>&lt;p&gt;Everyone knows Chinese summers are hot. Fewer people know how completely the country empties of comfortable places: in May, &lt;strong&gt;255 of the 359 cities&lt;/strong&gt; we track sit in a pleasant band; in July, that number falls to &lt;strong&gt;27&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we measured
&lt;/h2&gt;

&lt;p&gt;We hold monthly climate normals for &lt;strong&gt;359 Chinese cities&lt;/strong&gt; — 4,308 data points covering temperature, rainfall and humidity for 2021 to 2025. For each city we defined "comfortable" as a mean temperature between &lt;strong&gt;10 and 26°C&lt;/strong&gt; with rainfall below &lt;strong&gt;75% of that city's wettest month&lt;/strong&gt;. That second condition is the one that matters: a mild month that happens to be the rainy season is not a month you want to walk a city in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The months, ranked by how much of the country works
&lt;/h2&gt;

&lt;p&gt;May &lt;strong&gt;255&lt;/strong&gt; · October &lt;strong&gt;250&lt;/strong&gt; · April &lt;strong&gt;242&lt;/strong&gt; · September &lt;strong&gt;214&lt;/strong&gt; · March &lt;strong&gt;192&lt;/strong&gt; · November &lt;strong&gt;173&lt;/strong&gt; · June &lt;strong&gt;119&lt;/strong&gt; · February &lt;strong&gt;59&lt;/strong&gt; · December &lt;strong&gt;56&lt;/strong&gt; · January &lt;strong&gt;53&lt;/strong&gt; · August &lt;strong&gt;45&lt;/strong&gt; · July &lt;strong&gt;27&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The shape is not symmetric. Spring and autumn are wide open — almost the entire country qualifies. July and August are not "hotter versions of May"; they are a different country.&lt;/p&gt;

&lt;h2&gt;
  
  
  What July actually leaves you
&lt;/h2&gt;

&lt;p&gt;The 27 cities that pass in July are not the ones on the postcards. They are places at altitude, on the northern edge, or in the mountains of the southwest: Lishui at 26.0°C, Tongren at 25.7°C, Hanzhong at 25.3°C, Alashan at 25.6°C with 20mm of rain for the month, Altay at 25.4°C with 14mm. Beijing, Shanghai and Guangzhou are nowhere near the list.&lt;/p&gt;

&lt;p&gt;That is the useful part. "China is hot in July" is a sentence that tells a traveller nothing. "27 of 359 cities are comfortable in July, and here they are" tells them where to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  And January inverts it
&lt;/h2&gt;

&lt;p&gt;January leaves 53 cities, and almost all of them are on one coastline: Qionghai at 20.2°C, Wanning at 19.7°C, Dongfang at 19.1°C, Sanya at 18.9°C, Haikou at 18.7°C, plus Zhuhai, Beihai and Zhanjiang on the Guangdong and Guangxi coast. In winter the comfortable part of China is a strip about 200km wide in the south.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we publish this
&lt;/h2&gt;

&lt;p&gt;Climate data for China is easy to find per city and almost impossible to compare across cities, because every source uses different normals and different definitions. Holding one dataset of 359 cities on one set of five years makes questions like "where should I go in July" answerable rather than guessable.&lt;/p&gt;

&lt;p&gt;The full set is free to browse, no signup and no email wall: &lt;a href="https://sinostay.com" rel="noopener noreferrer"&gt;sinostay.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The working note with the full numbers and our method is at &lt;a href="https://sinostay.com/journal/3.html" rel="noopener noreferrer"&gt;sinostay.com/journal/3.html&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>travel</category>
      <category>china</category>
      <category>datascience</category>
      <category>climate</category>
    </item>
    <item>
      <title>Building a 9,718-hotel database for foreign travellers in China</title>
      <dc:creator>jiang Wang</dc:creator>
      <pubDate>Wed, 04 Nov 2026 07:55:14 +0000</pubDate>
      <link>https://dev.to/sinostay/building-a-9718-hotel-database-for-foreign-travellers-in-china-249d</link>
      <guid>https://dev.to/sinostay/building-a-9718-hotel-database-for-foreign-travellers-in-china-249d</guid>
      <description>&lt;p&gt;We maintain a dataset of hotels in China that will actually let a foreigner check in — right now &lt;strong&gt;9,718 properties across 359 cities&lt;/strong&gt;, plus 7,003 attractions, 34,504 restaurants and 9,120 shopping locations with English names and coordinates.&lt;/p&gt;

&lt;p&gt;Two things turned out to be harder than collecting the data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Images.&lt;/strong&gt; Every hotel needs a photo that is hotlinkable, fast and cheap to serve. We moved all of them onto object storage behind our own domain, converted them to WebP, and hashed the bytes so identical photos collapse into a single object. A typical hotel page loads around thirty images.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Showing an address to a taxi driver.&lt;/strong&gt; A hotel listing is only half the problem; the other half is saying "this one" to someone who does not read English. Every listing carries a Chinese-name card — Chinese name, Chinese address, city — generated from the same record that renders the page.&lt;/p&gt;

&lt;p&gt;The third problem was not technical. Scrapers pulled every image we had: one tool alone fetched 1.98 million of them in a week, and another swept every internal cache-key path we exposed. We now allow real search engines and refuse everything else, including requests with no user agent at all.&lt;/p&gt;

&lt;p&gt;The whole thing runs on Cloudflare Workers with D1 for the database and R2 for images — about 6,700 indexable URLs served from one 9,000-line worker file.&lt;/p&gt;

&lt;p&gt;If you are working on travel data for China, the underlying set is free to browse — no signup, no email wall.&lt;/p&gt;

&lt;p&gt;Full dataset and methodology: &lt;a href="https://sinostay.com/journal/1.html" rel="noopener noreferrer"&gt;https://sinostay.com/journal/1.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>performance</category>
      <category>travel</category>
    </item>
  </channel>
</rss>
