Skill: seanrea/ridb-search (version 1.0.0)

--- name: ridb-search description: Search the Recreation Information Database (RIDB) for campgrounds and recreation facilities near a location. Use when finding campgrounds, recreation areas, or federal facilities by location/radius. Supports geocoding (city names) and lat/lon coordinates. --- # RIDB Search Search recreation.gov's database for campgrounds and facilities near a location. ## Setup Requires a free RIDB API key: 1. Go to https://ridb.recreation.gov/profile 2. Sign up and generate an API key 3. Set environment variable: `export RIDB_API_KEY=your_key_here` ## Usage Search by location name (auto-geocodes): ```bash python scripts/search.py --location "Bend, OR" --radius 50 python scripts/search.py -l "Yosemite Valley" -r 25 --camping-only ``` Search by coordinates: ```bash python scripts/search.py --lat 44.0582 --lon -121.3153 --radius 50 ``` ### Options | Flag | Description | |------|-------------| | `--location, -l` | Location name to geocode (e.g., "Bend, OR") | | `--lat` | Latitude (use with --lon) | | `--lon` | Longitude (use with --lat) | | `--radius, -r` | Search radius in miles (default: 50) | | `--limit` | Max results (default: 50) | | `--camping-only` | Filter to camping facilities | | `--reservable-only` | Filter to reservable facilities | | `--json` | Output JSON (for programmatic use) | ### Output Human-readable (default): ``` 📍 Geocoded 'Bend, OR' to 44.0582, -121.3153 Found 23 facilities within 50 miles ------------------------------------------------------------ 🏕️ Tumalo State Park ID: 234567 | ✅ Reservable Org: Oregon State Parks URL: https://www.recreation.gov/camping/campgrounds/234567 ``` JSON output (`--json`): ```json { "query": {"latitude": 44.0582, "longitude": -121.3153, "radius_miles": 50}, "total_count": 23, "facilities": [ { "id": "234567", "name": "Tumalo State Park", "reservable": true, "url": "https://www.recreation.gov/camping/campgrounds/234567" } ] } ``` ## Notes - RIDB contains federal recreation data; some state/private campgrounds may not be listed - The `id` field is the campground ID used for availability checks on recreation.gov - Radius is in miles (RIDB native unit) - Geocoding uses OpenStreetMap/Nominatim (free, no key required)

path: references/api-notes.md
content: # RIDB API Notes

## Endpoints

Base URL: `https://ridb.recreation.gov/api/v1`

| Endpoint | Description |
|----------|-------------|
| `/facilities` | Search all facilities |
| `/facilities/{id}` | Get facility details |
| `/facilities/{id}/campsites` | Get campsites at a facility |
| `/recareas` | Search recreation areas |
| `/activities` | List activity types |

## Authentication

Pass API key in header: `[REDACTED_SECRET]`

## Facility Search Parameters

| Param | Type | Description |
|-------|------|-------------|
| `latitude` | float | Center point latitude |
| `longitude` | float | Center point longitude |
| `radius` | int | Miles from center |
| `limit` | int | Max results (default 50) |
| `offset` | int | Pagination offset |
| `activity` | string | Activity ID filter (9 = Camping) |
| `query` | string | Text search |

## Activity IDs (Common)

| ID | Activity |
|----|----------|
| 9 | Camping |
| 14 | Hiking |
| 5 | Biking |
| 11 | Fishing |
| 25 | Swimming |

## Rate Limits

- No documented rate limits for authenticated requests
- Be respectful; avoid hammering the API

## Availability API (Different!)

RIDB gives you facility metadata. For **availability**, use the recreation.gov availability API:

```
GET https://www.recreation.gov/api/camps/availability/campground/{facilityId}/month
?start_date=2024-07-01T00:00:00.000Z
```

This returns campsite-level availability for that month. No API key needed.

## References

- RIDB Portal: https://ridb.recreation.gov
- Get API Key: https://ridb.recreation.gov/profile
- recreation.gov: https://www.recreation.gov


path: README.md
content: # RIDB Search

A Python CLI for searching the Recreation Information Database (RIDB) to find federal campgrounds and recreation facilities near a location.

## Overview

RIDB is the authoritative database behind recreation.gov. This tool searches it to find:

- **Campground IDs** needed for `recgov-availability`
- Facility metadata (reservable status, managing agency)
- Nearby recreation areas

Use this to discover campgrounds, then check availability with `recgov-availability`.

### Coverage

- National Park Service facilities
- USDA Forest Service campgrounds
- Bureau of Land Management sites
- Army Corps of Engineers recreation areas
- Bureau of Reclamation facilities

For state parks, use `reserveamerica` instead.

## Prerequisites

- Python 3.8+
- **RIDB API key** (free)

### Getting an API Key

1. Go to https://ridb.recreation.gov/profile
2. Create an account
3. Generate an API key
4. Set the environment variable:

```bash
export RIDB_API_KEY=your_key_here
```

## Quick Start

```bash
cd /Users/doop/moltbot/skills/ridb-search

# Search by location name
python3 scripts/search.py --location "Newport, Oregon" --radius 30

# Filter to camping only
python3 scripts/search.py -l "Yosemite Valley" -r 25 --camping-only

# JSON output for scripts
python3 scripts/search.py -l "Bend, OR" --camping-only --json
```

## CLI Options

```bash
python3 scripts/search.py [options]
```

| Option | Description |
|--------|-------------|
| `-l, --location` | Location to geocode (e.g., "Bend, OR") |
| `--lat` | Latitude (use with --lon) |
| `--lon` | Longitude (use with --lat) |
| `-r, --radius` | Search radius in miles (default: 50) |
| `--limit` | Max results (default: 50) |
| `--camping-only` | Filter to camping facilities |
| `--reservable-only` | Filter to reservable facilities |
| `--api-key` | RIDB API key (or use env var) |
| `--json` | JSON output |

## Examples

### Find campgrounds near Newport, OR

```bash
python3 scripts/search.py -l "Newport, Oregon" --camping-only
```

Output:
```
📍 Geocoded 'Newport, Oregon' to 44.6368, -124.0534

Found 34 facilities within 50 miles
------------------------------------------------------------

🏕️  TILLICUM
   ID: 233965 | ✅ Reservable
   Org: Siuslaw National Forest
   URL: https://www.recreation.gov/camping/campgrounds/233965

🏕️  CAPE PERPETUA
   ID: 233900 | ✅ Reservable
   Org: Siuslaw National Forest
   URL: https://www.recreation.gov/camping/campgrounds/233900
```

### JSON output for scripting

```bash
python3 scripts/search.py -l "Bend, OR" --camping-only --reservable-only --json
```

```json
{
  "query": {
    "latitude": 44.0582,
    "longitude": -121.3153,
    "radius_miles": 50
  },
  "total_count": 47,
  "facilities": [
    {
      "id": "232089",
      "name": "TUMALO STATE PARK",
      "type": "Campground",
      "reservable": true,
      "latitude": 44.1272,
      "longitude": -121.3308,
      "description": "...",
      "url": "https://www.recreation.gov/camping/campgrounds/232089",
      "parent_org": "Oregon State Parks"
    }
  ]
}
```

## Workflow: Search → Check Availability → Book

```bash
# 1. Find campgrounds near your destination
python3 scripts/search.py -l "Newport, OR" --camping-only --reservable-only

# 2. Note the IDs, then check availability
python3 ../recgov-availability/scripts/check.py \
  -c 233965 233900 \
  --start 2026-07-10 \
  --nights 2

# 3. Book on recreation.gov
open "https://www.recreation.gov/camping/campgrounds/233965"
```

## Architecture

```
scripts/
└── search.py      # Single-file CLI (stdlib only)

references/
└── (add API docs here if needed)
```

### API Endpoint

```
GET https://ridb.recreation.gov/api/v1/facilities
```

Headers:
- `apikey`: Your RIDB API key

Query Parameters:
- `latitude`, `longitude`: Search center
- `radius`: Miles
- `limit`: Max results
- `activity`: Activity ID filter (9 = Camping)

## Technical Notes

### Geocoding

Uses OpenStreetMap Nominatim for free location→coordinates conversion. No API key needed.

### Rate Limiting

RIDB doesn't document rate limits, but be reasonable (~1 req/sec for bulk operations).

### No Dependencies

Uses only Python standard library.

## Combining with Other Skills

| Task | Skill |
|------|-------|
| Find campground IDs | **ridb-search** (this) |
| Check rec.gov availability | recgov-availability |
| Search state parks | reserveamerica |

## License

MIT


path: scripts/search.py
content: #!/usr/bin/env python3
"""
Search RIDB (Recreation Information Database) for campgrounds near a location.

Requires: RIDB_API_KEY environment variable
Get a free key at: https://ridb.recreation.gov/profile

Usage:
    python search.py --location "Bend, OR" --radius 50
    python search.py --lat 44.0582 --lon -121.3153 --radius 50
    python search.py --location "Yosemite Valley" --radius 25 --limit 20
"""

import argparse
import json
import os
import sys
import urllib.parse
import urllib.request
from typing import Optional

RIDB_BASE_URL = "https://ridb.recreation.gov/api/v1"
NOMINATIM_URL = "https://nominatim.openstreetmap.org/search"


def geocode(location: str) -> tuple[float, float]:
    """Convert a location name to lat/lon using Nominatim (OpenStreetMap)."""
    params = urllib.parse.urlencode({
        "q": location,
        "format": "json",
        "limit": 1,
    })
    url = f"{NOMINATIM_URL}?{params}"
    
    req = urllib.request.Request(url, headers={
        "User-Agent": "ridb-search/1.0 (camping availability checker)"
    })
    
    with urllib.request.urlopen(req, timeout=10) as resp:
        data = json.loads(resp.read().decode())
    
    if not data:
        raise ValueError(f"Could not geocode location: {location}")
    
    return float(data[0]["lat"]), float(data[0]["lon"])


def search_facilities(
    lat: float,
    lon: float,
    radius: int = 50,
    limit: int = 50,
    activity: Optional[str] = None,
    [REDACTED_SECRET] = None,
) -> dict:
    """
    Search RIDB for facilities (campgrounds) near a location.
    
    Args:
        lat: Latitude
        lon: Longitude
        radius: Search radius in miles (default 50)
        limit: Max results to return (default 50)
        activity: Filter by activity (e.g., "CAMPING" or activity ID "9")
        api_key: RIDB API key (falls back to RIDB_API_KEY env var)
    
    Returns:
        dict with RECDATA (list of facilities) and METADATA
    """
    [REDACTED_SECRET] or os.environ.get("RIDB_API_KEY")
    if not api_key:
        raise ValueError(
            "RIDB API key required. Set RIDB_API_KEY env var or pass --api-key.\n"
            "Get a free key at: https://ridb.recreation.gov/profile"
        )
    
    params = {
        "latitude": lat,
        "longitude": lon,
        "radius": radius,
        "limit": limit,
    }
    
    # Filter to camping facilities
    if activity:
        params["activity"] = activity
    
    query = urllib.parse.urlencode(params)
    url = f"{RIDB_BASE_URL}/facilities?{query}"
    
    req = urllib.request.Request(url, headers={
        "apikey": api_key,
        "Accept": "application/json",
    })
    
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode())


def format_facility(facility: dict) -> dict:
    """Extract key info from a facility record."""
    return {
        "id": facility.get("FacilityID"),
        "name": facility.get("FacilityName"),
        "type": facility.get("FacilityTypeDescription"),
        "reservable": facility.get("Reservable", False),
        "latitude": facility.get("FacilityLatitude"),
        "longitude": facility.get("FacilityLongitude"),
        "description": (facility.get("FacilityDescription") or "")[:200],
        "url": f"https://www.recreation.gov/camping/campgrounds/{facility.get('FacilityID')}",
        "parent_org": facility.get("ParentOrgName"),
    }


def main():
    parser = argparse.ArgumentParser(
        description="Search RIDB for campgrounds near a location"
    )
    parser.add_argument(
        "--location", "-l",
        help="Location to search near (e.g., 'Bend, OR')"
    )
    parser.add_argument("--lat", type=float, help="Latitude")
    parser.add_argument("--lon", type=float, help="Longitude")
    parser.add_argument(
        "--radius", "-r",
        type=int,
        default=50,
        help="Search radius in miles (default: 50)"
    )
    parser.add_argument(
        "--limit",
        type=int,
        default=50,
        help="Max results (default: 50)"
    )
    parser.add_argument(
        "--camping-only",
        action="store_true",
        help="Filter to camping facilities only"
    )
    parser.add_argument(
        "--reservable-only",
        action="store_true",
        help="Filter to reservable facilities only"
    )
    parser.add_argument("--api-key", help="RIDB API key (or set RIDB_API_KEY)")
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output raw JSON"
    )
    
    args = parser.parse_args()
    
    # Get coordinates
    if args.location:
        try:
            lat, lon = geocode(args.location)
            if not args.json:
                print(f"📍 Geocoded '{args.location}' to {lat:.4f}, {lon:.4f}\n")
        except Exception as e:
            print(f"Error geocoding location: {e}", file=sys.stderr)
            sys.exit(1)
    elif args.lat is not None and args.lon is not None:
        lat, lon = args.lat, args.lon
    else:
        print("Error: Provide --location or both --lat and --lon", file=sys.stderr)
        sys.exit(1)
    
    # Search RIDB
    try:
        # Activity ID 9 = Camping
        activity = "9" if args.camping_only else None
        result = search_facilities(
            lat=lat,
            lon=lon,
            radius=args.radius,
            limit=args.limit,
            activity=activity,
            [REDACTED_SECRET],
        )
    except ValueError as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)
    except urllib.error.HTTPError as e:
        print(f"API Error ({e.code}): {e.read().decode()}", file=sys.stderr)
        sys.exit(1)
    
    facilities = result.get("RECDATA", [])
    
    # Filter to reservable if requested
    if args.reservable_only:
        facilities = [f for f in facilities if f.get("Reservable")]
    
    if args.json:
        output = {
            "query": {
                "latitude": lat,
                "longitude": lon,
                "radius_miles": args.radius,
            },
            "total_count": result.get("METADATA", {}).get("RESULTS", {}).get("TOTAL_COUNT", 0),
            "facilities": [format_facility(f) for f in facilities],
        }
        print(json.dumps(output, indent=2))
    else:
        total = result.get("METADATA", {}).get("RESULTS", {}).get("TOTAL_COUNT", 0)
        print(f"Found {total} facilities within {args.radius} miles\n")
        print("-" * 60)
        
        for f in facilities:
            info = format_facility(f)
            reservable = "✅ Reservable" if info["reservable"] else "❌ Not reservable"
            print(f"\n🏕️  {info['name']}")
            print(f"   ID: {info['id']} | {reservable}")
            if info["parent_org"]:
                print(f"   Org: {info['parent_org']}")
            print(f"   URL: {info['url']}")
        
        if not facilities:
            print("\nNo facilities found. Try increasing --radius.")


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)