To solve this optimization problem, we first need to define the decision variables and the objective function. Let's denote:

- $x_1$ as the number of ads on taxis,
- $x_2$ as the number of ads on buses,
- $x_3$ as the number of ads on privately owned cars.

The objective is to maximize viewership, which can be calculated based on the expected viewership per ad type. Given data:

- Cost and viewership for taxi: $500, 5000 viewers
- Cost and viewership for bus: $1000, 12000 viewers
- Cost and viewership for privately owned car: $300, 2000 viewers

The total budget is $20,000.

Constraints:
1. Budget constraint: $500x_1 + 1000x_2 + 300x_3 \leq 20000$
2. Bus company limit: $x_2 \leq 8$
3. At most 30% of ads on taxis: $x_1 \leq 0.3(x_1 + x_2 + x_3)$
4. At least 20% of ads on privately owned cars: $x_3 \geq 0.2(x_1 + x_2 + x_3)$

The objective function to maximize viewership is: $5000x_1 + 12000x_2 + 2000x_3$

Let's implement this problem using Gurobi in Python:

```python
from gurobipy import *

# Create a model
m = Model("Ad_Optimization")

# Define decision variables
x1 = m.addVar(name='taxi_ads', vtype=GRB.INTEGER, lb=0)
x2 = m.addVar(name='bus_ads', vtype=GRB.INTEGER, lb=0)
x3 = m.addVar(name='private_car_ads', vtype=GRB.INTEGER, lb=0)

# Set the objective function
m.setObjective(5000*x1 + 12000*x2 + 2000*x3, GRB.MAXIMIZE)

# Add constraints
m.addConstr(500*x1 + 1000*x2 + 300*x3 <= 20000, name='budget')
m.addConstr(x2 <= 8, name='bus_limit')
m.addConstr(x1 <= 0.3*(x1 + x2 + x3), name='taxi_percentage')
m.addConstr(x3 >= 0.2*(x1 + x2 + x3), name='private_car_percentage')

# Optimize the model
m.optimize()

# Print results
for v in m.getVars():
    print(f"{v.varName}: {v.x}")
```