How Location-Based Channels Work in BitChat: Geohash Precision Explained

BitChat implements hierarchical location-based channels by encoding GPS coordinates into base-32 geohash strings and truncating them to six distinct precision levels, enabling scoped chat rooms from building-level to regional-wide coverage.

Location-based channels in BitChat leverage geohash encoding to enable proximity-based communication on the Nostr network. The permissionlesstech/bitchat repository implements a tiered precision system that converts latitude and longitude coordinates into base-32 geohash strings of varying lengths, creating distinct chat scopes from individual buildings to entire regions.

Mapping Precision Levels to Geographic Scope

BitChat defines six distinct precision levels that map geohash string lengths to real-world geographic areas. These levels are enumerated in bitchat/Protocols/LocationChannel.swift as GeohashChannelLevel.

The GeohashChannelLevel Enum

The GeohashChannelLevel enum establishes a direct correlation between precision integers and geographic scope:

// bitchat/Protocols/LocationChannel.swift
enum GeohashChannelLevel: CaseIterable, Codable, Equatable {
    case building, block, neighborhood, city, province, region
    
    var precision: Int {                     // ↔ geohash length
        switch self {                        // 8‑char = building, …, 2‑char = region
        case .building:      return 8
        case .block:         return 7
        case .neighborhood:  return 6
        case .city:          return 5
        case .province:      return 4
        case .region:        return 2
        }
    }
}

Each precision value corresponds to the exact character length of the geohash string. For example, .city uses 5 characters (e.g., "9q8yy"), while .building uses 8 characters (e.g., "9q8yyz29").

Encoding Coordinates to Channel Identifiers

The geohash generation logic resides in bitchat/Protocols/Geohash.swift, which provides the lightweight encoder used by LocationStateManager.swift.

Geohash Generation in Swift

The encoder converts latitude and longitude into base-32 strings of specified lengths:

// bitchat/Protocols/Geohash.swift
static func encode(latitude: Double, longitude: Double, precision: Int) -> String { … }

When LocationStateManager.swift needs to generate a channel for a user's current position, it calls this method with the selected level's precision value. A precision of 8 produces a string precise enough to identify individual buildings, while precision 2 covers massive regional areas.

Constructing the Channel Architecture

BitChat wraps geohash data in the GeohashChannel struct and exposes it through the ChannelID enum for Nostr protocol integration.

The GeohashChannel Struct

Defined in bitchat/Protocols/LocationChannel.swift, this structure creates unique channel identifiers by combining the precision level with the truncated geohash:

// bitchat/Protocols/LocationChannel.swift
struct GeohashChannel: Codable, Equatable, Hashable, Identifiable {
    let level: GeohashChannelLevel
    let geohash: String               // already truncated to `level.precision`
    
    var id: String { "\(level)-\(geohash)" }
    var displayName: String { "\(level.displayName) • \(geohash)" }
}

The displayName property provides user-facing labels rendered in LocationChannelsSheet.swift, while LocationChannelsModel.swift manages the mapping between geohash lengths and their human-readable titles.

ChannelID and Nostr Integration

The ChannelID enum distinguishes between mesh networks and location-based channels, exposing the geohash via the nostrGeohashTag property:

// bitchat/Protocols/LocationChannel.swift
enum ChannelID: Equatable, Codable {
    case mesh
    case location(GeohashChannel)      // ↔ Nostr “#g” tag
    
    var nostrGeohashTag: String? {
        switch self { case .mesh: nil; case .location(let ch): ch.geohash }
    }
}

When publishing messages, BitChat extracts the geohash string and includes it as a g tag in Nostr events, ensuring only subscribers within the same geographic cell receive the messages.

End-to-End Implementation Example

To create a location-based channel at city-level precision, implement the following workflow as used in LocationStateManager.swift:

import BitChat

// 1️⃣ Choose a precision level (e.g., city)
let level = GeohashChannelLevel.city          // precision = 5

// 2️⃣ Encode current coordinates to a geohash of that length
let latitude = 37.7749
let longitude = -122.4194
let geohash = Geohash.encode(latitude: latitude,
                             longitude: longitude,
                             precision: level.precision)   // → "9q8yy"

// 3️⃣ Build the channel object
let cityChannel = GeohashChannel(level: level, geohash: geohash)

// 4️⃣ Activate it
let activeChannel = ChannelID.location(cityChannel)

// 5️⃣ Retrieve the Nostr tag for publishing
if let tag = activeChannel.nostrGeohashTag {
    // publish event with `tags: [["g", tag]]`
    // Only peers in San Francisco area receive this
}

Summary

BitChat's location-based channel system operates through these core mechanisms:

  • Six precision levels defined in GeohashChannelLevel, mapping geohash string lengths from 2 characters (region) to 8 characters (building)
  • Base-32 geohash encoding via Geohash.encode() in bitchat/Protocols/Geohash.swift, converting GPS coordinates into hierarchical grid identifiers
  • Structured channel wrapping through GeohashChannel, which pairs precision metadata with truncated geohash strings for ChannelID.location
  • Nostr protocol integration using nostrGeohashTag, which exposes geohash data as g tags for geographic message routing

Frequently Asked Questions

What geohash precision should I use for neighborhood-level chat?

Use GeohashChannelLevel.neighborhood, which corresponds to a 6-character geohash precision. According to the implementation in bitchat/Protocols/LocationChannel.swift, this level creates a channel covering approximately 1.2km by 600m, suitable for community discussions within a few city blocks.

How does BitChat convert my GPS coordinates into a channel ID?

The app passes your latitude and longitude to Geohash.encode(latitude:longitude:precision:) along with the selected level's precision integer. The resulting geohash string gets wrapped in a GeohashChannel struct, which generates a unique id property combining the level name and geohash value as implemented in bitchat/Protocols/LocationChannel.swift.

Can users switch between different geohash precision levels dynamically?

Yes. The LocationChannelsSheet.swift UI component allows users to select any of the six GeohashChannelLevel cases. When a user switches levels, LocationStateManager.swift recalculates the geohash using the new precision value and emits an updated ChannelID.location, immediately re-scoping messages to the new geographic cell.

Why does BitChat use geohash instead of raw coordinates for location channels?

Geohash provides a standardized, hierarchical encoding that converts complex latitude/longitude pairs into short, shareable strings with predictable precision boundaries. The base-32 algorithm enables BitChat to truncate coordinates to specific lengths, creating discrete geographic "cells" that function as stable channel identifiers across the distributed Nostr relay network without exposing exact user locations.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →