Skip to content

Commit 3ba58aa

Browse files
committed
feat: add postal code detection endpoint
1 parent 61c57a7 commit 3ba58aa

File tree

10 files changed

+125
-32
lines changed

10 files changed

+125
-32
lines changed

README.md

+28-9
Original file line numberDiff line numberDiff line change
@@ -63,28 +63,47 @@ The fastest way to use it privately on PaaS available
6363
<img alt="Deploy with Render" src="https://render.com/images/deploy-to-render-button.svg" height="32" />
6464
</a>
6565

66-
## Basic Usage
66+
## Endpoints
67+
68+
### Search by Place Name
6769

6870
```
6971
[ENDPOINT] /search
7072
```
7173
7274
<pre>
73-
[GET] <a href="https://kodepos.vercel.app/?q=danasari">http://localhost:3000/search/?q=danasari</a>
75+
[GET] <a href="https://kodepos.vercel.app/search/?q=danasari">http://localhost:3000/search/?q=danasari</a>
76+
</pre>
77+
78+
#### Query strings
79+
80+
| Params | Description | Required |
81+
| ------ | ----------- | :------: |
82+
| q | keywords | [x] |
83+
84+
### Search by Coordinates
85+
86+
```
87+
[ENDPOINT] /detect
88+
```
89+
90+
<pre>
91+
[GET] <a href="https://kodepos.vercel.app/detect/?latitude=-6.547052&longitude=107.3980201">http://localhost:3000/detect/?latitude=-6.547052&longitude=107.3980201</a>
7492
</pre>
7593
7694
#### Query strings
7795
78-
| params | description | required |
79-
| ------ | :---------: | :------: |
80-
| q | keywords | `true` |
96+
| Params | Description | Required |
97+
| --------- | ----------- | :------: |
98+
| latitude | - | [x] |
99+
| longitude | - | [x] |
81100
82-
### Example of Use
101+
### Basic Usage
83102
84103
#### Request
85104
86105
<pre>
87-
curl -XGET '<a href="https://kodepos.vercel.app/?q=danasari">http://localhost:3000/search/?q=danasari</a>'
106+
curl -XGET '<a href="https://kodepos.vercel.app/search/?q=danasari">http://localhost:3000/search/?q=danasari</a>'
88107
</pre>
89108
90109
#### Response
@@ -159,8 +178,8 @@ List of server APIs ready to use publicly
159178
> [!IMPORTANT]
160179
> For production usage, we recommend deploying it on your own and not using the list below. The list below can be used for development or learning purposes only!
161180
162-
- [https://kodepos.vercel.app](https://kodepos.vercel.app/?q=danasari) `latest`
163-
- [https://kodepos.onrender.com](https://kodepos.onrender.com/?q=danasari) `latest`
181+
- [https://kodepos.vercel.app](https://kodepos.vercel.app/search/?q=danasari) `latest`
182+
- [https://kodepos.onrender.com](https://kodepos.onrender.com/search/?q=danasari) `latest`
164183

165184
### License
166185

app/controllers/detect.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { DetectQueries } from '../../types'
2+
import { nearestDetection } from '../helpers/kodepos'
3+
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
4+
import { createSpecResponse, sendBadRequest, sendNotFound } from '../helpers/spec'
5+
6+
export const detect = (app: FastifyInstance) => {
7+
return async (request: FastifyRequest<{ Querystring: DetectQueries }>, reply: FastifyReply) => {
8+
const { latitude, longitude } = request.query
9+
10+
if (!latitude || !longitude) {
11+
return sendBadRequest(reply, "The 'latitude' and 'longitude' parameters is required.")
12+
}
13+
14+
const result = nearestDetection(app.data, latitude, longitude)
15+
16+
if (!result) {
17+
return sendNotFound(reply)
18+
}
19+
20+
const response = createSpecResponse(result)
21+
22+
reply.header('Cache-Control', 's-maxage=86400, stale-while-revalidate=604800')
23+
return reply.send(response)
24+
}
25+
}

app/controllers/home.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import qs from 'node:querystring'
2-
import { KeywordOptions } from '../../types'
2+
import { SearchQueries } from '../../types'
33
import type { FastifyReply, FastifyRequest } from 'fastify'
44

55
export const home = async (
6-
request: FastifyRequest<{ Querystring: KeywordOptions }>,
6+
request: FastifyRequest<{ Querystring: SearchQueries }>,
77
reply: FastifyReply
88
) => {
99
const { q } = request.query

app/controllers/search.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { KeywordOptions } from '../../types'
1+
import { SearchQueries } from '../../types'
22
import { createSpecResponse, sendBadRequest } from '../helpers/spec'
33
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
44

55
export const search = (app: FastifyInstance) => {
6-
return async (request: FastifyRequest<{ Querystring: KeywordOptions }>, reply: FastifyReply) => {
6+
return async (request: FastifyRequest<{ Querystring: SearchQueries }>, reply: FastifyReply) => {
77
const { q } = request.query
88
// TODO: search by province, regency, or district
99

1010
if (!q) {
11-
return sendBadRequest(reply)
11+
return sendBadRequest(reply, "The 'q' parameter is required.")
1212
}
1313

1414
const keywords = q

app/helpers/kodepos.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { DataResult } from '../../types'
2+
3+
export const createFullText = (data: DataResult) => {
4+
const keys = ['code', 'village', 'district', 'regency', 'province']
5+
const combinations: string[] = []
6+
7+
keys.forEach((a, x) => {
8+
keys.forEach((b, y) => {
9+
if (x !== y) {
10+
combinations.push(`${data[a]} ${data[b]}`)
11+
}
12+
})
13+
})
14+
15+
return combinations.join(' ')
16+
}
17+
18+
export const haversineDistance = (lat1: number, lon1: number, lat2: number, lon2: number) => {
19+
const R = 6371 // earth radius in km
20+
const dLat = ((lat2 - lat1) * Math.PI) / 180
21+
const dLon = ((lon2 - lon1) * Math.PI) / 180
22+
const a =
23+
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
24+
Math.cos((lat1 * Math.PI) / 180) *
25+
Math.cos((lat2 * Math.PI) / 180) *
26+
Math.sin(dLon / 2) *
27+
Math.sin(dLon / 2)
28+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
29+
30+
return R * c // distance in km
31+
}
32+
33+
export const nearestDetection = (
34+
data: DataResult[],
35+
lat: number,
36+
lon: number
37+
): DataResult | null => {
38+
let nearest: DataResult | null = null
39+
let smallestDistance = Infinity
40+
41+
data.forEach((item) => {
42+
const distance = haversineDistance(lat, lon, item.latitude, item.longitude)
43+
44+
if (distance < smallestDistance) {
45+
nearest = item
46+
smallestDistance = distance
47+
nearest.distance = distance
48+
}
49+
})
50+
51+
return nearest
52+
}

app/helpers/spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ export const sendNotFound = (reply: FastifyReply) => {
2222
})
2323
}
2424

25-
export const sendBadRequest = (reply: FastifyReply) => {
25+
export const sendBadRequest = (reply: FastifyReply, message?: string) => {
2626
return reply.status(400).send({
2727
statusCode: 400,
2828
code: 'BAD_REQUEST',
29-
message: "The 'q' parameter must be filled.",
29+
message: message || "The request body doesn't contain valid data.",
3030
})
3131
}

start/core.ts

+3-15
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,9 @@ import * as path from 'path'
33
import { routes } from './routes'
44
import * as fs from 'node:fs/promises'
55
import type { DataResult } from '../types'
6+
import { createFullText } from '../app/helpers/kodepos'
67
import type { FastifyInstance, FastifyPluginOptions } from 'fastify'
78

8-
const createFullText = (data: DataResult) => {
9-
const keys = ['code', 'village', 'district', 'regency', 'province']
10-
const combinations: string[] = []
11-
12-
keys.forEach((a, x) => {
13-
keys.forEach((b, y) => {
14-
if (x !== y) {
15-
combinations.push(`${data[a]} ${data[b]}`)
16-
}
17-
})
18-
})
19-
20-
return combinations.join(' ')
21-
}
22-
239
const load = async (app: FastifyInstance, _: FastifyPluginOptions) => {
2410
const text = await fs.readFile(path.resolve('data/kodepos.json'), { encoding: 'utf-8' })
2511
const json: DataResult[] = JSON.parse(text)
@@ -35,6 +21,8 @@ const load = async (app: FastifyInstance, _: FastifyPluginOptions) => {
3521
})
3622

3723
app.decorate('fuse', fuse)
24+
app.decorate('data', json)
25+
3826
routes(app)
3927
}
4028

start/routes.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { home } from '../app/controllers/home'
22
import type { FastifyInstance } from 'fastify'
3+
import { detect } from '../app/controllers/detect'
34
import { search } from '../app/controllers/search'
45

56
export const routes = (app: FastifyInstance) => {
67
app.get('/', home)
78
app.get('/search', search(app))
9+
app.get('/detect', detect(app))
810
}

types/fastify/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ import { DataResult } from '..'
44
declare module 'fastify' {
55
export interface FastifyInstance {
66
fuse: Fuse<DataResult>
7+
data: DataResult[]
78
}
89
}

types/index.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
export type KeywordOptions = {
1+
export type SearchQueries = {
22
q: string
33
province?: string
44
regency?: string
55
district?: string
66
}
77

8+
export type DetectQueries = {
9+
latitude: number
10+
longitude: number
11+
}
12+
813
export type DataResult = {
914
province: string
1015
regency: string
@@ -16,4 +21,5 @@ export type DataResult = {
1621
elevation: number
1722
timezone: string
1823
fulltext?: string
24+
distance?: number
1925
}

0 commit comments

Comments
 (0)