How BitChat's Geohash-Based Location Channels Function at Multiple Precision Levels

BitChat creates hierarchical location-based chat rooms by encoding geographic coordinates into geohash strings of varying lengths (2–8 characters), where each length maps to a specific precision level from region down to individual building.

BitChat is an open-source messaging application built on the Nostr protocol that implements geohash-based location channels to scope conversations geographically. According to the permissionlesstech/bitchat source code, the system partitions the globe into six distinct precision levels—region, province, city, neighborhood, block, and building—by truncating geohash strings to specific character counts, enabling users to broadcast messages to location-based communities of varying granularity.

Understanding Geohash Precision Levels

BitChat organizes geographic chat rooms through a precise mapping between human-readable location scales and geohash string lengths. Shorter strings cover larger areas, while longer strings isolate specific venues.

The GeohashChannelLevel Enumeration

In bitchat/Protocols/LocationChannel.swift, the GeohashChannelLevel enum defines six hierarchical scopes and their corresponding geohash precisions:

enum GeohashChannelLevel: CaseIterable, Codable, Equatable {
    case building, block, neighborhood, city, province, region
    
    var precision: Int {
        switch self {
        case .building:      return 8  // ~20m × 20m
        case .block:         return 7  // ~150m × 150m
        case .neighborhood:  return 6  // ~1.2km × 0.6km
        case .city:          return 5  // ~5km × 5km
        case .province:      return 4  // ~40km × 20km
        case .region:        return 2  // ~2,500km × 5,000km
        }
    }
}

Each precision value represents the exact character length of the base-32 geohash string. A region channel uses only 2 characters (covering thousands of kilometers), while a building channel uses 8 characters (targeting a specific ~20-meter square).

Converting Coordinates to Geohash Strings

The Geohash.swift file contains a lightweight encoder that transforms latitude and longitude into truncated geohash identifiers. The encode method accepts a precision parameter to control the returned string length:

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

When a user selects a precision level, the app calls this method with the corresponding level.precision value. For example, encoding coordinates 37.7749, -122.4194 at precision: 5 returns "9q8yy", representing a city-level channel in San Francisco.

Building Location Channels and Nostr Integration

BitChat wraps geohash strings in GeohashChannel objects that integrate directly with the Nostr protocol's tagging system.

The GeohashChannel Structure

The GeohashChannel struct in LocationChannel.swift bundles a precision level with its corresponding geohash string:

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 geohash property stores the pre-truncated string, ensuring the channel ID remains consistent for all participants in that geographic cell.

ChannelID and Nostr Tag Generation

The ChannelID enum distinguishes between mesh networks and location-based channels, exposing a nostrGeohashTag property for protocol integration:

enum ChannelID: Equatable, Codable {
    case mesh
    case location(GeohashChannel)
    
    var nostrGeohashTag: String? {
        switch self {
        case .mesh: return nil
        case .location(let channel): return channel.geohash
        }
    }
}

When publishing messages, BitChat extracts this tag and attaches it to Nostr events as a g tag (e.g., ["g", "9q8yy"]). This ensures that only peers subscribed to the same geohash cell receive the messages, creating geographically isolated chat rooms on the global relay network.

End-to-End Implementation Flow

The complete workflow spans multiple files in the codebase, from UI selection to message publication.

Step 1: Select Precision Level

Users choose a granularity through the interface defined in LocationChannelsSheet.swift, which maps GeohashChannelLevel cases to human-readable labels using LocationChannelsModel.swift for display name resolution.

Step 2: Encode Location

LocationStateManager.swift handles the coordinate conversion:

let level = GeohashChannelLevel.block  // 7-character precision
let geohash = Geohash.encode(
    latitude: currentLocation.lat,
    longitude: currentLocation.lon,
    precision: level.precision
)  // Returns something like "9q8yymz"

Step 3: Construct and Activate Channel

The app instantiates a channel object and activates it for messaging:

let channel = GeohashChannel(level: level, geohash: geohash)
let activeChannel = ChannelID.location(channel)

// Retrieve Nostr tag
if let tag = activeChannel.nostrGeohashTag {
    // Publish event with tags: [["g", tag]]
    publishToNostr(geohashTag: tag)
}

Summary

  • Six precision levels map to geohash lengths of 2–8 characters, ranging from region (~2,500km) down to building (~20m).
  • GeohashChannelLevel in LocationChannel.swift defines the precision mapping, while Geohash.swift handles base-32 encoding.
  • GeohashChannel objects combine a precision level with a truncated geohash string to create unique channel identifiers.
  • The nostrGeohashTag property outputs the g tag required for Nostr protocol integration, scoping messages to specific geographic cells.
  • LocationStateManager.swift orchestrates the encoding process when users switch between precision levels.

Frequently Asked Questions

How does BitChat determine which precision level covers a specific geographic area?

BitChat uses the GeohashChannelLevel enum in bitchat/Protocols/LocationChannel.swift to map preset character lengths to geographic scopes. Each level corresponds to a specific geohash string length: building (8 characters), block (7), neighborhood (6), city (5), province (4), and region (2). These lengths follow the standard geohash specification where each additional character increases precision by approximately 4-5 times.

What prevents messages from leaking between different precision levels?

Each GeohashChannel generates a unique nostrGeohashTag containing the exact truncated geohash string. When publishing to Nostr relays, BitChat includes this value as a g tag in the event. Since subscribers filter by exact tag matches, a user subscribed to the 5-character city-level geohash "9q8yy" will not receive messages tagged with the 7-character block-level geohash "9q8yymz", even though both cover the same physical area.

Can users dynamically switch between location precision levels without restarting the app?

Yes. The LocationStateManager.swift service monitors the user's selected GeohashChannelLevel and recomputes the geohash in real-time using Geohash.encode(). When the user selects a different precision from LocationChannelsSheet.swift, the app immediately generates a new ChannelID.location with the updated geohash length, allowing seamless switching between broad regional discussions and hyperlocal block-level conversations.

Which source file contains the actual base-32 geohash encoding algorithm?

The encoding logic resides in bitchat/Protocols/Geohash.swift, which implements the standard base-32 geohash algorithm. This file contains the encode(latitude:longitude:precision:) static method used across the application to convert GPS coordinates into truncated geohash strings of the exact length specified by the current GeohashChannelLevel.

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 →