Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 6 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
node_modules/
/.pnp
.pnp.*
.yarn/*
Expand All @@ -11,14 +11,14 @@
!.yarn/versions

# testing
/coverage
coverage/

# next.js
/.next/
/out/
.next/
out/

# production
/build
build/

# misc
.DS_Store
Expand Down Expand Up @@ -47,4 +47,4 @@ AGENT.md
.cursor

# ETL raw data
scripts/raw/
apps/web/scripts/raw/
4 changes: 2 additions & 2 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ out
build
node_modules
pnpm-lock.yaml
public/data
src/components/ui
apps/web/public/data
apps/web/src/components/ui
44 changes: 27 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
- [shadcn/ui](https://ui.shadcn.com/) 4 / [mapcn](https://mapcn.vercel.app/)
- [MapLibre GL JS](https://maplibre.org/) -- 台灣行政區地圖
- [TypeScript](https://www.typescriptlang.org/) 6
- [pnpm](https://pnpm.io/) 套件管理
- [pnpm](https://pnpm.io/) workspace monorepo(`apps/web` 應用 + `packages/core` 核心邏輯)

## 快速開始

Expand Down Expand Up @@ -64,39 +64,49 @@ pnpm dev

### 資料流程(ETL)

原始郵政資料放置於 `scripts/raw/`(不納入版本控制)。ETL 腳本(`scripts/etl.ts`)處理兩份來源:
原始郵政資料放置於 `apps/web/scripts/raw/`(不納入版本控制)。ETL 腳本(`apps/web/scripts/etl.ts`)處理兩份來源:

```
CityCountyData.json + AllData.json --> public/data/base.json(縣市/鄉鎮市區索引)
public/data/roads/{zip3}.json(各區路街清單)
CityCountyData.json + AllData.json --> apps/web/public/data/base.json(縣市/鄉鎮市區索引)
apps/web/public/data/roads/{zip3}.json(各區路街清單)

rall1.txt (CSV) --> public/data/zip-ranges/{zip3}.json(郵遞區號範圍規則)
rall1.txt (CSV) --> apps/web/public/data/zip-ranges/{zip3}.json(郵遞區號範圍規則)
```

- `base.json` 在建置時靜態匯入,包含所有縣市與鄉鎮市區
- `roads/{zip3}.json` 與 `zip-ranges/{zip3}.json` 於使用者選擇鄉鎮市區時按需載入並快取

### 郵遞區號比對

`src/lib/lookup-zipcode.ts` 以特異性評分(specificity scoring)比對地址與郵遞區號範圍規則。條件越多(巷段、弄段、門牌範圍、單雙號、樓層)的規則分數越高,優先採用。支援門牌之號(如「5之1」)與樓層限制等進階比對。
`packages/core/src/lookup-zipcode.ts`(`@zipkit/core` 套件)以特異性評分(specificity scoring)比對地址與郵遞區號範圍規則。條件越多(巷段、弄段、門牌範圍、單雙號、樓層)的規則分數越高,優先採用。支援門牌之號(如「5之1」)與樓層限制等進階比對。

### 狀態管理

`src/hooks/use-address-state.ts` 以 `useReducer` 管理聯動表單狀態。選擇上層欄位時自動重設下層欄位。路街與郵遞區號資料透過 `src/lib/data-loader.ts` 按需載入並快取於記憶體中。
`apps/web/src/hooks/use-address-state.ts` 以 `useReducer` 管理聯動表單狀態。選擇上層欄位時自動重設下層欄位。路街與郵遞區號資料透過 `apps/web/src/lib/data-loader.ts` 按需載入並快取於記憶體中。

## 專案結構

本專案為 pnpm workspace monorepo。

```
src/
app/ # Next.js App Router 頁面
components/ # UI 元件
hooks/ # React hooks(地址狀態管理)
lib/ # 工具函式(郵遞區號比對、資料載入)
scripts/
etl.ts # ETL 資料轉換腳本
raw/ # 原始郵政資料(不納入版本控制)
public/
data/ # 預先產生的靜態 JSON
apps/
web/ # Next.js 應用(前端 + SSG 區頁)
src/
app/ # App Router 頁面
components/ # UI 元件
hooks/ # React hooks(地址狀態管理)
lib/ # web 專屬工具(資料載入、地圖、slug)
scripts/
etl.ts # ETL 資料轉換腳本
raw/ # 原始郵政資料(不納入版本控制)
public/
data/ # 預先產生的靜態 JSON
packages/
core/ # @zipkit/core:地址核心邏輯(無框架依賴)
src/
lookup-zipcode.ts # 3+3 郵遞區號特異性評分比對
format-english-address.ts # UPU 格式英譯
types.ts # 共用領域型別
```

## 資料來源
Expand Down
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions next.config.ts → apps/web/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
transpilePackages: ["@zipkit/core"],
rewrites() {
return {
beforeFiles: [
Expand Down
52 changes: 52 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"typecheck": "tsc --noEmit",
"etl": "tsx scripts/etl.ts",
"shadcn:diff": "for f in src/components/ui/*.tsx; do name=$(basename \"$f\" .tsx); echo \"=== $name ===\"; pnpm shadcn add \"$name\" --diff 2>&1 || pnpm shadcn add \"@mapcn/$name\" --diff 2>&1; echo; done",
"shadcn:update": "for f in src/components/ui/*.tsx; do name=$(basename \"$f\" .tsx); pnpm shadcn add \"$name\" --overwrite 2>&1 || pnpm shadcn add \"@mapcn/$name\" --overwrite 2>&1; done"
},
"dependencies": {
"@base-ui/react": "^1.5.0",
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
"@zipkit/core": "workspace:*",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"lucide-react": "^1.18.0",
"maplibre-gl": "^5.24.0",
"next": "16.2.9",
"next-themes": "^0.4.6",
"react": "19.2.7",
"react-dom": "19.2.7",
"shadcn": "^4.11.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"topojson-client": "^3.1.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.1",
"@types/geojson": "^7946.0.16",
"@types/node": "^25.9.3",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@types/topojson-client": "^3.1.5",
"@types/topojson-specification": "^1.0.5",
"eslint": "^9.39.4",
"eslint-config-next": "16.2.9",
"eslint-config-prettier": "^10.1.8",
"tailwindcss": "^4.3.1",
"taiwan-atlas": "^2021.9.20",
"tsx": "^4.22.4",
"typescript": "^6.0.3",
"typescript-eslint": "^8.61.1"
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading