```python
import time
from typing import Callable, Dict, Any

class JobQueue:
    """
    A job queue implementation that supports retry logic and exponential backoff.
    """
    MAX_RETRIES = 3

    def __init__(self):
        # Stores job data and state: {job_id: {'data': ..., 'retries': 0}}
        self.jobs: Dict[str, Dict[str, Any]] = {}

    def add_job(self, job_id: str, data: Dict[str, Any]):
        """
        Adds a new job to the queue.
        """
        if job_id in self.jobs:
            raise ValueError(f"Job ID '{job_id}' already exists.")
        self.jobs[job_id] = {'data': data, 'retries': 0}
        print(f"Job {job_id} added to queue.")

    def process_job(self, job_id: str, processor: Callable) -> bool:
        """
        Processes a job, implementing retry logic and exponential backoff on failure.

        Args:
            job_id: The ID of the job to process.
            processor: The callable function that executes the job logic.

        Returns:
            True if the job succeeded, False if all retries were exhausted.
        """
        if job_id not in self.jobs:
            print(f"Error: Job ID {job_id} not found.")
            return False

        job_state = self.jobs[job_id]
        job_data = job_state['data']
        retries = job_state['retries']

        print(f"--- Starting processing for Job {job_id} (Attempt {retries + 1}) ---")

        while retries <= self.MAX_RETRIES:
            try:
                # Execute the job processor
                processor(job_data)
                print(f"✅ Job {job_id} succeeded on attempt {retries + 1}.")
                return True

            except Exception as e:
                print(f"❌ Job {job_id} failed on attempt {retries + 1}. Error: {e}")

                if retries == self.MAX_RETRIES:
                    print(f"🛑 Job {job_id} failed after {self.MAX_RETRIES + 1} attempts. Giving up.")
                    return False

                # Calculate backoff delay (2^retries seconds)
                # Retries start at 0.
                # Retry 1 (retries=0): 2^0 = 1s
                # Retry 2 (retries=1): 2^1 = 2s
                # Retry 3 (retries=2): 2^2 = 4s
                delay = 2 ** retries
                job_state['retries'] += 1
                
                print(f"⏳ Retrying Job {job_id} in {delay} seconds (simulated backoff)...")
                # time.sleep(delay) # Uncomment this line for actual sleeping
                
                retries = job_state['retries']

        return False # Should be unreachable if logic is correct, but serves as a safeguard

# ==============================================================================
# Example Usage
# ==============================================================================

# 1. Successful Job Example
print("\n" + "="*50)
print("TEST CASE 1: Successful Job")
print("="*50)
queue1 = JobQueue()
queue1.add_job("job_success", {"url": "https://example.com"})

def successful_fetch(data):
    print(f"  [Processor] Fetching {data['url']} successfully.")
    # Simulate successful operation
    pass

success = queue1.process_job("job_success", successful_fetch)
print(f"\nResult for job_success: {success}")


# 2. Failure and Retry Example (Fails twice, succeeds on third attempt)
print("\n" + "="*50)
print("TEST CASE 2: Successful Retry")
print("="*50)
queue2 = JobQueue()
queue2.add_job("job_retry_success", {"url": "http://fail.com"})

attempt_count = 0
def flaky_fetch(data):
    global attempt_count
    attempt_count += 1
    if attempt_count <= 2:
        raise ConnectionError(f"Network timeout on attempt {attempt_count}")
    print(f"  [Processor] Fetching {data['url']} successfully after initial failures.")

success = queue2.process_job("job_retry_success", flaky_fetch)
print(f"\nResult for job_retry_success: {success}")


# 3. Exhausted Retry Example (Fails all 4 attempts)
print("\n" + "="*50)
print("TEST CASE 3: Exhausted Retries")
print("="*50)
queue3 = JobQueue()
queue3.add_job("job_failure_max", {"url": "http://alwaysfail.com"})

def always_fail_fetch(data):
    print(f"  [Processor] Attempting to fetch {data['url']}...")
    raise IOError("Service Unavailable")

success = queue3.process_job("job_failure_max", always_fail_fetch)
print(f"\nResult for job_failure_max: {success}")
```