Update city search to return district/locality string suggestions
All checks were successful
build-and-deploy / build-deploy (push) Successful in 36s

This commit is contained in:
androidlover5842
2026-02-07 22:20:48 +05:30
parent c5dc32d9af
commit e15c72a159
3 changed files with 118 additions and 10 deletions

View File

@@ -128,8 +128,11 @@ AUTH + SYSTEM APIS
What it does:
- Searches city names by prefix from local pincode directory table.
- Returns unique city + state rows only.
- Searches district and locality names by prefix from local pincode directory table.
- Returns a JSON array of unique strings in this format: "NAME, STATE".
- District matches are ranked first.
- If district matches exist, locality results from those districts are also included.
- Locality suffixes like H.O / S.O / B.O are trimmed in output.
- Uses local DB only (no external API call in this endpoint).
Request body:

View File

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import java.util.Locale
@RestController
class GeoSearch(
@@ -20,20 +21,63 @@ class GeoSearch(
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestParam("q") q: String,
@RequestParam("limit", required = false, defaultValue = "20") limit: Int
): List<CityStateResponse> {
): List<String> {
requirePrincipal(principal)
val query = q.trim()
if (query.length < 2) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "q must be at least 2 characters")
}
val boundedLimit = limit.coerceIn(1, 100)
return indiaPincodeCityStateRepo
.searchCityStateByPrefix(query, PageRequest.of(0, boundedLimit))
.map { CityStateResponse(city = it.city, state = it.state) }
val districtRows = indiaPincodeCityStateRepo
.searchDistrictStateByPrefix(query, PageRequest.of(0, boundedLimit))
val localityPrefixRows = indiaPincodeCityStateRepo
.searchLocalityStateByPrefix(query, PageRequest.of(0, boundedLimit))
val results = linkedSetOf<String>()
districtRows.forEach { row ->
val district = row.district.trim().ifBlank { return@forEach }
val state = row.state.trim().ifBlank { return@forEach }
results.add("$district, $state")
}
localityPrefixRows.forEach { row ->
val formatted = formatLocalityWithState(row.locality, row.state) ?: return@forEach
results.add(formatted)
}
if (districtRows.isNotEmpty() && results.size < boundedLimit) {
val districtKeys = districtRows
.map { it.district.trim().uppercase(Locale.ROOT) }
.filter { it.isNotBlank() }
.toSet()
if (districtKeys.isNotEmpty()) {
val extraRows = indiaPincodeCityStateRepo.findLocalityStateByDistricts(
districtKeys,
PageRequest.of(0, boundedLimit * 5)
)
extraRows.forEach { row ->
if (results.size >= boundedLimit) return@forEach
val formatted = formatLocalityWithState(row.locality, row.state) ?: return@forEach
results.add(formatted)
}
}
}
return results.take(boundedLimit)
}
}
data class CityStateResponse(
val city: String,
val state: String
)
private fun formatLocalityWithState(localityRaw: String, stateRaw: String): String? {
val state = stateRaw.trim()
if (state.isBlank()) return null
val locality = localityRaw
.trim()
.replace(LOCALITY_SUFFIX_REGEX, "")
.replace(WHITESPACE_REGEX, " ")
.trim()
if (locality.isBlank()) return null
return "$locality, $state"
}
private val LOCALITY_SUFFIX_REGEX =
Regex("""\s+(H\.O|S\.O|B\.O|G\.P\.O|PO|HO|SO|BO|GPO)(\s*\(.*\))?$""", RegexOption.IGNORE_CASE)
private val WHITESPACE_REGEX = Regex("\\s+")

View File

@@ -42,6 +42,57 @@ interface IndiaPincodeCityStateRepo : JpaRepository<IndiaPincodeCityState, UUID>
@Param("prefix") prefix: String,
pageable: Pageable
): List<CityStateRow>
@Query(
"""
select p.city as district,
p.state as state
from IndiaPincodeCityState p
where trim(p.city) <> ''
and trim(p.state) <> ''
and lower(p.city) like lower(concat(:prefix, '%'))
group by p.city, p.state
order by p.city asc, p.state asc
"""
)
fun searchDistrictStateByPrefix(
@Param("prefix") prefix: String,
pageable: Pageable
): List<DistrictStateRow>
@Query(
"""
select p.locality as locality,
p.state as state
from IndiaPincodeCityState p
where trim(p.locality) <> ''
and trim(p.state) <> ''
and lower(p.locality) like lower(concat(:prefix, '%'))
group by p.locality, p.state
order by p.locality asc, p.state asc
"""
)
fun searchLocalityStateByPrefix(
@Param("prefix") prefix: String,
pageable: Pageable
): List<LocalityStateRow>
@Query(
"""
select p.locality as locality,
p.state as state
from IndiaPincodeCityState p
where trim(p.locality) <> ''
and trim(p.state) <> ''
and upper(trim(p.city)) in :districts
group by p.locality, p.state
order by p.locality asc, p.state asc
"""
)
fun findLocalityStateByDistricts(
@Param("districts") districts: Set<String>,
pageable: Pageable
): List<LocalityStateRow>
}
interface PincodeCityStateCandidate {
@@ -54,3 +105,13 @@ interface CityStateRow {
val city: String
val state: String
}
interface DistrictStateRow {
val district: String
val state: String
}
interface LocalityStateRow {
val locality: String
val state: String
}