Understanding the Geohash Precision Hierarchy in BitChat: Block, Neighborhood, and City Levels Explained
BitChat uses a six-tier geohash precision hierarchy where longer geohash strings represent smaller geographic areas, with only region (2 chars), province (4 chars), and city (5 chars) levels broadcasting presence data to protect user privacy.
BitChat's location-based messaging system organizes channels using geohash precision hierarchy — a system that maps string lengths to real-world geographic scales. This architecture, implemented in the permissionlesstech/bitchat repository, balances location-based discovery with strong privacy protections by restricting presence broadcasts to coarser precision levels.
How Geohash Precision Levels Map to Geographic Areas
BitChat defines six precision levels in [LocationChannel.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/LocationChannel.swift). Each GeohashChannelLevel case maps to a specific character count in the encoded geohash string:
| Precision Level | Geohash Length | Approximate Area |
|---|---|---|
| Region | 2 characters | ~5,000 km² (country-scale) |
| Province | 4 characters | ~160 km² (state-scale) |
| City | 5 characters | ~40 km² (city-scale) |
| Neighborhood | 6 characters | ~5 km² (district) |
| Block | 7 characters | ~0.6 km² (city block) |
| Building | 8 characters | ~0.08 km² (single building) |
The GeohashChannelLevel enum implements this mapping through a computed precision property:
enum GeohashChannelLevel: CaseIterable, Codable, Equatable {
case building, block, neighborhood, city, province, region
var precision: Int {
switch self {
case .building: return 8
case .block: return 7
case .neighborhood: return 6
case .city: return 5
case .province: return 4
case .region: return 2
}
}
}
Notice that precision values skip 3 — this maintains alignment with standard geohash resolution jumps where each character approximately halves the longitude and latitude ranges.
Encoding Locations into Geohash Channels
When users create or join location-based channels, BitChat encodes coordinates using Geohash.encode(latitude:longitude:precision:). The precision parameter determines which hierarchy level the channel occupies.
import BitChat
// Encode San Francisco coordinates to different precision levels
let lat = 37.78
let lon = -122.41
// City-level precision (5 characters)
let cityHash = Geohash.encode(
latitude: lat,
longitude: lon,
precision: GeohashChannelLevel.city.precision
)
// Result: "9q8yy"
// Block-level precision (7 characters)
let blockHash = Geohash.encode(
latitude: lat,
longitude: lon,
precision: GeohashChannelLevel.block.precision
)
// Result: "9q8yym8"
The additional characters in blockHash compared to cityHash narrow the geographic bounding box from approximately 40 km² down to roughly 0.6 km² — a 60× reduction in area coverage.
Privacy-First Presence Broadcasting Rules
BitChat's geohash precision hierarchy directly determines which channels receive automatic presence heartbeats. The [GeohashPresenceService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/GeohashPresenceService.swift) file enforces a strict allowlist:
private let allowedPrecisions: Set<Int> = [
GeohashChannelLevel.region.precision, // 2
GeohashChannelLevel.province.precision, // 4
GeohashChannelLevel.city.precision // 5
]
This means:
- Region, province, and city channels broadcast ephemeral presence (Nostr kind 20001)
- Neighborhood, block, and building channels never auto-broadcast presence
The exclusion protects against location triangulation attacks — an adversary monitoring neighborhood-level or finer heartbeats could potentially track a user's movements with building-level accuracy.
Detecting Broadcast Eligibility in Code
let service = GeohashPresenceService.shared
service.start() // Initializes heartbeat loop
// Check if a channel will broadcast presence
let cityChannel = GeohashChannel(level: .city, geohash: "9q8yy")
let blockChannel = GeohashChannel(level: .block, geohash: "9q8yym8")
service.allowedPrecisions.contains(cityChannel.geohash.count) // true — broadcasts
service.allowedPrecisions.contains(blockChannel.geohash.count) // false — silent
UI Behavior Across Precision Levels
BitChat's interface adapts based on whether presence data exists for a given geohash precision hierarchy level, as documented in [GeohashPresenceSpec.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/GeohashPresenceSpec.md).
Participant Count Display
| Precision | Presence Broadcast | UI Display |
|---|---|---|
| Region, Province, City | Yes | Exact count: "42 people" |
| Neighborhood, Block, Building | No | Ambiguous: "[? people]" |
The "[? people]" indicator signals to users that BitChat intentionally withholds this information for privacy protection, not due to a technical failure.
High-Precision Sharing Warnings
When users attempt to share neighborhood-level or finer channels, [LocationChannelsSheet.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Views/LocationChannelsSheet.swift) triggers an operational security warning. This alerts users that sharing a geohash of 6+ characters reveals their approximate location to recipients — a risk not present when sharing city-level or coarser channels.
Why the Hierarchy Excludes Intermediate Precisions
Two design decisions in BitChat's geohash precision hierarchy warrant explanation:
-
Missing precision value 3: Standard geohash implementations use character counts that double resolution (roughly) with each step. Precision 3 (~400 km²) falls between province and city scales without clear semantic mapping, so BitChat jumps directly from 4 to 5.
-
No 1-character geohash: Single-character geohashes span ~20,000 km² — larger than most continents. This provides minimal location value while creating relay routing inefficiencies, so the hierarchy begins at 2 characters (region).
These omissions keep the precision levels semantically meaningful and technically efficient.
Testing the Hierarchy Implementation
The [GeohashPresenceTests.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/GeohashPresenceTests.swift) suite validates that:
- All six
GeohashChannelLevelcases map to correct integer precisions allowedPrecisionscontains only 2, 4, and 5- Channels with geohash lengths 6, 7, or 8 correctly fail presence eligibility checks
- UI components render "[? people]" for high-precision channels
// Example test pattern from the test suite
func testHighPrecisionChannelsExcluded() {
let highPrecisions = [6, 7, 8] // neighborhood, block, building
for length in highPrecisions {
XCTAssertFalse(
service.allowedPrecisions.contains(length),
"Precision \(length) should not broadcast presence"
)
}
}
Summary
- Geohash precision hierarchy in BitChat uses 2–8 character strings mapped to six geographic scales from region (~5,000 km²) down to building (~0.08 km²)
- The
GeohashChannelLevelenum in [LocationChannel.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/LocationChannel.swift) defines this mapping with precision values of 2, 4, 5, 6, 7, and 8 - Presence broadcasts are restricted to region (2), province (4), and city (5) via
allowedPrecisionsin [GeohashPresenceService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchateat/Services/GeohashPresenceService.swift) - Neighborhood, block, and building channels operate silently — no heartbeats, ambiguous participant counts, and sharing warnings
- This architecture prevents location tracking while maintaining useful proximity-based channel discovery at broader scales
Frequently Asked Questions
Why doesn't BitChat broadcast presence for block-level channels?
Block-level geohashes (7 characters, ~0.6 km²) reveal precise location information that could enable stalking or surveillance. By excluding precisions 6, 7, and 8 from allowedPrecisions, BitChat ensures users cannot be tracked at neighborhood-scale resolution or finer. The trade-off is that users cannot see real-time participant counts for these channels.
How does BitChat determine which precision level a geohash belongs to?
The app inspects the string length of any geohash and matches it against the precision property of GeohashChannelLevel. A 5-character geohash maps to .city, a 6-character geohash maps to .neighborhood, and so forth. This reverse mapping happens throughout the codebase whenever channel metadata needs interpretation.
Can users manually override the presence broadcast restrictions?
No. The allowedPrecisions set in [GeohashPresenceService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/GeohashPresenceService.swift) is hardcoded as a private constant. Users cannot configure their client to broadcast fine-grained presence, ensuring consistent privacy protection across the network. Attempting to modify this would require forking the codebase and running a non-standard client.
What happens when I share a building-level channel with someone?
[LocationChannelsSheet.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Views/LocationChannelsSheet.swift) detects geohash lengths ≥ 6 and displays a warning dialog before completing the share action. The warning explains that the recipient will learn your approximate location within ~80 meters. Users must confirm this disclosure before the share proceeds.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →