development
打造「台灣郵遞區號查詢」:一個純靜態 side project 的開發記

前陣子要寄一份文件,需要填 3+3(六碼)郵遞區號。我打開中華郵政官網,先用下拉選單選縣市,再選鄉鎮市區,接著在一長串路名裡找到自己那條,最後還要看門牌號落在哪個區間。查一個地址點了五、六下,填錯還要重來。
還有「中文地址英譯」。填國外表單或訂單常常需要英文地址,官網有這個功能,但要先輸入一組圖形驗證碼才給你查一次。查兩個地址就要認兩次驗證碼。
這些資料都是公開的,查一次卻要這麼多步驟。所以我利用零碎時間做了一個 台灣郵遞區號查詢。你輸入完整地址,它直接把結果給你,沒有下拉選單,也沒有驗證碼。這篇記錄一下這個 side project 的動機、做了哪些東西,還有幾個技術決策。
做了什麼
整個站的做法就是一句話:你把完整地址貼進去,它把答案給你。
郵遞區號查詢:輸入像「臺北市中正區重慶南路一段122號」這樣的完整地址,畫面就顯示 5 碼(3+2)與最新的 3+3 碼(6 碼),旁邊有一鍵複製。台北市和臺北市這種「台」和「臺」的寫法都查得到。

地址中翻英:輸入中文地址,轉成中華郵政官方寫法的英文地址:
3F., No. 122, Sec. 1, Chongqing S. Rd., Zhongzheng Dist., Taipei City 100005, Taiwan (R.O.C.)
一樣免驗證碼、附上 3+3 郵遞區號、可以一鍵複製。查完郵遞區號之後,還可以一鍵把同一個地址帶去中翻英,不用再貼一次。

除了這兩個工具,站上還有全台 22 縣市、368 個鄉鎮市區的郵遞區號一覽頁,以及一些寄件相關的文章。
接下來講幾個做的時候花比較多時間的技術決策。
決策一:整站純靜態,沒有執行期 API
這種查詢工具,一般會做成前端一個輸入框、後端一支 API 查資料庫。但我一開始就決定不做後端。
整站用 Vue 3 加 vite-ssg 做純靜態 SSG,約 400 個頁面在 build 階段全部預先渲染成 HTML,部署在 Cloudflare Workers 的靜態資源上,沒有執行期 API。每一個縣市頁、每一個行政區頁都是一份已經寫好的 HTML,Google 爬進來就是完整內容,不用等 JavaScript 跑完才看到郵遞區號表格。
好處是沒有伺服器要顧、沒有資料庫要備份,維運成本接近零,流量大也是 CDN 在扛靜態檔。對一個下班之後做的 side project 來說,不用維運這點就值得。
代價是所有查詢邏輯都要搬到前端,資料也要塞進瀏覽器。這就帶出下面兩個問題。
決策二:資料只用中華郵政官方檔案,連 DBF 都自己解
做郵遞區號工具最省事的做法,是去 GitHub 抓別人整理好的 CSV。我試了幾個開源資料集,大多是舊的,而且只有 3+2 五碼,沒有最新的 3+3。所以我定了一條規則:資料只用中華郵政官方檔案,不用第三方來源。
問題是官方的 3+3 資料不是一份 CSV,而是包在一個中華郵政的 Windows 應用程式裡,資料存在 rall1.dbf 這個 dBASE 格式的檔案,而且是 Big5 編碼。為了把這 8 萬多筆資料拿出來,我自己寫了一個 DBF 的 binary parser,直接讀檔頭、逐筆解 record,再把 Big5 解碼成 UTF-8。
官方資料裡的門牌也不是一個地址對一個碼,而是一堆範圍描述字串,長這樣:
全
單 5號以上
雙 2號至 168號
162號
102號 2樓以下
所以光有資料還不夠,我還要寫一個 parser 去理解「單號 5 號以上」「雙號 2 號到 168 號」「162 號這一戶」「102 號的 2 樓以下自成一區」這些規則,才能把使用者輸入的門牌對到正確的 3+3 碼。這部分後面會再提到。
決策三:地址英譯不能用音譯拼,要查官方對照表
地址中翻英這塊,我本來以為用拼音套件把中文轉成羅馬拼音就好。實際做下去才發現,台灣的路名英譯有不少要查表才對得到的特例,不是規則能推出來的。
幾個例子:
- 羅斯福路是 Roosevelt Rd.,不是 Luosifu
- 凱達格蘭大道是 Ketagalan Blvd.
這些是歷史地名和外來語音譯,用拼音演算法拼不出來。所以英譯這邊我一樣只用官方資料,也就是中華郵政的「路街中英對照檔」,大約 3 萬筆。流程是先查官方對照表,查得到就用官方寫法;查不到的少數路名,才 fallback 到拼音音譯,並且在畫面上標註這筆是機器音譯、非官方。
這也是官網那個中翻英要放驗證碼的原因,它其實是即時去查一份對照表。而我把整份對照表在 build 階段就處理好、切成靜態資料,查詢就不需要驗證碼,也不需要後端。
決策四:build 階段切資料塊,順便讓 build 幫我守體積
決策一說整站純靜態、資料要塞進瀏覽器,但 8 萬筆郵遞區號加 3 萬筆英譯,全部打包成一個 JSON 丟給前端不可行,首次載入會太慢。
我的做法是在 build 階段,把資料依行政區切成一塊一塊的 JSON,前端只在使用者查到某個區時,才去 fetch 那一個區的檔案。你查中正區,就只下載中正區那幾 KB,不會把全台灣的資料都拉下來。
切塊之後有個問題:資料會更新,哪天中華郵政多了一批門牌,某個區的檔案可能就變大,而我不會知道。所以我在 build 流程裡加了一道 gzip size budget,每一類檔案都設一個壓縮後的體積上限:
單一區的 3+3 明細 ≤ 30 KB (gzip)
搜尋索引 ≤ 25 KB (gzip)
縣市清單 ≤ 5 KB (gzip)
只要任何一個檔案超標,build 就直接失敗。這樣一來,效能預算就變成一條會擋下我的線,不會等上線後才發現某頁變慢。
那些台灣地址的邊角
最後講一下地址 parser。台灣地址的不規則比我想像的多,處理這些狀況大概花了我一半的開發時間。
「台」和「臺」:官方用「臺」,但多數人打「台」。台北和臺北、台中和臺中、台南和臺南、台東和臺東這四組,我在 build 索引時就同時產出兩種寫法,加上 runtime 也做 normalize,讓使用者怎麼打都查得到,網址則統一導到官方的「臺」字版本。
門牌副號「7之2號」:有些門牌是「23之1號」這種帶副號的。這時候不能只比對主號,要優先命中「23 之 1 號」這個更精確的範圍,對到才算。
樓層寫成「3樓之五」:門牌後面的樓層有人打阿拉伯數字、有人打中文數字。前面提到官方資料裡「102號 2樓以下」這種是真的會自成一個郵遞區,所以 parser 還要排序哪個範圍最精確,同樣精確度時再依樓層排,地下樓層排在最前面,跟中華郵政的預設一致。
這些規則單獨看都很瑣碎,但官網就是把這些狀況用一層層下拉選單推給使用者。我想做的是把它們吞進 parser 裡,使用者只要貼一個地址就好。
小結
這個 side project 的起點很單純,就是我自己覺得查郵遞區號很煩,做出來之後自己天天在用。回頭看,幾個技術決策都圍繞同一個想法:把麻煩留在 build 階段,把乾淨的結果留給使用者。用官方資料、自己解 DBF、build 階段切塊並守住體積、把台灣地址的各種狀況塞進 parser,這些工都是為了讓最後那個輸入框可以貼上、得到答案、複製。
如果你也常查郵遞區號或填英文地址,可以試試 台灣郵遞區號查詢 和 地址中翻英。這個工具也收在我的小工具站 pocketool 裡。