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

> Explore how BitChat's geohash-based location channels work across multiple precision levels, from broad regions to specific buildings. Learn about its hierarchical chat room system.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-07-30

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/LocationChannel.swift), the `GeohashChannelLevel` enum defines six hierarchical scopes and their corresponding geohash precisions:

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/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:

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/LocationChannel.swift) bundles a precision level with its corresponding geohash string:

```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 `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:

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/LocationChannelsSheet.swift), which maps `GeohashChannelLevel` cases to human-readable labels using [`LocationChannelsModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/LocationChannelsModel.swift) for display name resolution.

### Step 2: Encode Location

[`LocationStateManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/LocationStateManager.swift) handles the coordinate conversion:

```swift
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:

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/LocationChannel.swift) defines the precision mapping, while [`Geohash.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`.